多形態MVC式Web架構:完成實時響應
本文要點
- 在Web時代的前二十年,在用戶視圖及其現實或虛擬世界間的MVC可觀察的事件驅動同步已經不再發揮什么作用了。
- 近期的一些新進展使這一基礎理念得以在Web開發社區復蘇。
- dWMVC 和 pWMVC架構范式可以用于完成端到端變化觀察“事件環”去創建無縫高效的實時響應應用行為。
- 傳統中間件架構和新生代的服務器運動期環境都可被用于去完成這些實時響應行為。
- 非傳統服務運行環境和數據庫享有使用非標準化技術去創建創新解決方案的架構自由。
之前在《 多形態MVC式Web架構的分類 》中,我們闡述并討論了WMVC(基于Web的 MVC ) 架構范式的三個種類。它們是服務器端WMVC (sWMVC)、雙重WMVC (dWMVC)以及點對點WMVC (pWMVC)。 sWMVC通常本質上是靜態的,而其他兩個架構范式可用于構建實時響應的Web應用組件。這是其后續文章,在這篇文章中,我們將利用這兩個架構范式去設計和演示完全動態和響應式現代Web組件。
MVC架構方法的核心是實現用戶視圖與它們所反映的真實或虛擬世界之間同步的事件驅動的觀察者模式。該視圖(包括或未包括來自于用戶的額外指令)預期是反映世界的變化。在許多MVC實現中都體現了這一思想,從最初的桌面界面到現代增強和虛擬現實( AR 和 VR )。在《 多形態MVC式Web架構的分類 》的討論中提到,這個基本思想在Web的前二十年之后已經不再發揮什么作用了。在這段時間,Web應用以基于sWMVC的方法為主導。在最近幾年,它在WUI(Web用戶界面)應用開發社區中有所復興。這個新運動是由許多技術產品和標準協議驅動的。
在本文中,我們將運用一些新的進步去實現異步的、自然的、無縫的以及高效的從WUI到后端SoR原始記錄(source of record)變化觀察響應式“事件環”。這方面關鍵實用技術是:
以下討論相關的源碼可 點擊 在GitHub上獲取。
用戶故事
我們假定客戶想要一個基于瀏覽器的博客評論系統。該Web應用允許用戶觀看一個博客主題并發表評論。
下面是一張Web頁面的概念設計截圖,由三個子視圖構成。最上層那一塊顯示的是博客主題,其后是評論輸入和提交域。最后一塊區域負責顯示所有用戶輸入的評論。
圖1 博客評論設計截圖
該日志系統應該包括兩個很有特色的應用:
- 第一個應用會獲取博客評論的所有權并把它們存儲進集中式數據庫中。
- 第二個應用在集中式數據庫中不保存任何用戶評論,以確保用戶隱私及客戶責任。
該系統的第三個組件是把其他來源的博客評論整合到這個集中式數據庫中,它將在未來開發。
所有應用用戶應該有一個視圖永遠能看到最新的博客評論。
在一個用戶正在閱讀評論時,由其他用戶或通過自動化整合添加了新的評論,那就應該立即顯示在所有用戶Web頁面上,而不必他或她手工刷新。
系統架構
具有集中式數據庫的博客Web應用將用dWMVC范式予以設計和開發。總的來說,應用組件間的通訊將用AngularJS、SSE、InSoR和 CDC來實現。這些技術將使系統能夠響應任何對集中式數據庫中記錄的修改(通過這個Web應用或未來的集成模塊),并實時傳遞這種變化給最終用戶,概覽圖如圖2。
圖2 集中式實時博客Web應用系統架構
客戶端與服務器端之間的通訊基于的是HTTP和SSE協議,因為 InSoR 和 CDC 完全是在應用服務器和數據庫之間往返的。
第二個Web應用將以pWMVC模式實現(如圖3)。它將擔任一個使能者的角色,在不必改變內容所有權的情況下把用戶聚到一起。
圖3 點對點實時博客Web應用的系統架構
通過dWMVC實現的集中式Web應用
下面的圖4是基于dWMVC的博客Web應用設計概覽。在瀏覽器端,視圖和控制器組件是基于AngularJS的。兩個不同的服務器端技術棧組合被用于dWMVC模型組件的實現。左側的是傳統Java棧和J2EE架構,以及關系型數據庫 PostgresSQL 。NodeJS和 RethinkDB 那一側用于圖解基于JavaScript的服務器端運行環境和 NoSQL 數據庫的架構范式。這些不同的服務器端設計和實現代表了實現同一功能的兩種不同方法。除了NodeJS的異步特性,在InSoR 和 CDC中也存在特別明顯的差異,在NoSQL數據庫提供者中可自由控制架構,從而可以使用非標準化的技術去創建創新的解決方案(比如 lazy evaluation 和 lazy loading )。這兩種實現還提供了很多技術性選擇,以滿足Web開發社區(從傳統中間件實踐者到NodeJS/NoSQL 狂熱者)廣泛的興趣。
圖4 博客應用dWMVC設計模式的架構圖。客戶端WMVC視圖和控制器是基于AngularJS的。服務器端模型組件的兩個選擇是:Java-RDBMS(左側)和NodeJS-NoSQL(右側)。
dWMVC的視圖和控制器
該博客網頁是用AngularJS局部模板實現的。它是一個復合視圖,用于為博客日志的提交和顯示提供服務。
<div class="blocker1">
<h3>Topic: WMVC Real Time Reactive Fulfillment</h3>
</div>
<div id="castingId" class="blocker2">
<div>
<h4>Post a Comment:</h4>
</div>
<form id="commentFormId">
<div>
<input type="text" style="width: 30%" name="newCommentId" placeholder="What is in your mind?" ng-model="newComment"/>
<button role="submit" class="btn btn-default" ng-click="addComment()"><span class="glyphicon glyphicon-plus"></span>Send</button>
</div>
</form>
</div>
<div>
<h4>All Comments:</h4>
</div>
<div class="view" ng-switch on="comments.comments.length" ng-cloak>
<ul ng-switch-when="0">
<li>
<em>No comments yet available. Be the first to add a comment.</em>
</li>
</ul>
<ul ng-switch-default>
<li ng-repeat="comment in comments.comments">
<span>{{comment.comment}} </span>
</li>
</ul>
</div></code></pre>
該HTML頁面依賴于dWMVC控制器(如圖5)與服務器端的通信去增加新的評論,并為其他用戶刷新頁面。

圖5 博客評論應用的視圖和控制器組件。
為了為用戶顯示和刷新博客評論,該控制器:
- 通過HTTP之上的SSE連接后端服務器。
- 如果有的話,則異步接收和顯示所有已有的日志評論。
- 保持連接并監聽未來的SSE事件,它將更新的評論事件作為事件負載進行傳遞。
- 當SSE事件發生時,推送和綁定更新的日志評論到用戶的視圖頁面。
所有這些交互和反應是用以下代碼段實現的:
var dataHandler = function (event)
{
var data = JSON.parse(event.data);
console.log('Real time feeding => ' + JSON.stringify(data));
$scope.$apply(function ()
{
$scope.comments = data;
});
};
var eventSource = new EventSource('/wmvcapp/svc/comments/all');
eventSource.addEventListener('message', dataHandler, false);
當一名用戶增加一個新的評論時,它會直接傳送到服務器端用于處理:
$scope.addComment = function ()
{
var newInput = $scope.newComment.trim();
if (!newInput.length)
{
return;
}
var url = '/wmvcapp/svc/comments/cast';
$http.post(url, newInput);
$scope.newComment = '';
};</code></pre>
接下來,在下面將討論由服務器模型組件捕獲和處理它所關聯的數據變更。
dWMVC的Java和PostgreSQL模型組件
主要組件都包含在傳統技術棧內,一個基于Java的中間件應用程序庫組合和一個關系型數據庫,如圖6所示。

圖6 基于Java和PostgreSQL的dWMVC模型組件。
這些模型中的交互和響應如圖7所示。它展示了兩個訪問該博客應用的用戶。
(點擊放大圖像)

圖7 為查看博客評論的用戶提供實時惰性更新的一系列交互,基于的是Java和PostgreSQL關系型數據庫。
當一個用戶打開博客頁面時,dWMVC控制器立即實例化一個SSE實例,它啟動與服務器的通信以接收博客評論。其相關的服務器組件如下所示,注解了SSE請求和實現基于SSE的輸出。當服務器端組件接收到來自于dWMVC控制器基于SSE的請求時,它首先針對已有評論查詢一下數據庫,然后廣播一個異步 EventOutput 到該控制器,從而將該評論顯示給用戶瀏覽器。與此期間,為了接收在該PostgreSQL內對該博客主題后續變更的持續通知,該服務器端組件增加一個監聽以保持對PostgreSQL數據的“主題觀察者”的監聽。
@GET
@Path("/all")
@Produces(SseFeature.SERVER_SENT_EVENTS)
public EventOutput getAllComments() throws Exception
{
final EventOutput eventOutput = new EventOutput();
Statement sqlStatement = null;
//Query and return current data
String comments = BlogByPostgreSQL.getInstance().findComments(ConfigStringConstants.TOPIC_ID);
this.writeToEventOutput(comments, eventOutput);
//Listen to future change notifications
PGConnection conn = (PGConnection)BlogByPostgreSQL.getInstance().getConnection();
sqlStatement = conn.createStatement();
sqlStatement.execute("LISTEN topics_observer");
conn.addNotificationListener("topics_observer", new PGNotificationListener()
{
@Override
public void notification(int processId, String channelName, String payload)
{
JSONObject plJson = new JSONObject(payload);
String plComments = plJson.getJSONObject("topic_comments").toString();
writeToEventOutput(plComments, eventOutput);
}
});
return eventOutput;
}
private void writeToEventOutput(String comments, EventOutput eventOutput)
{
OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
eventBuilder.mediaType(MediaType.APPLICATION_JSON_TYPE);
if(comments == null || comments.trim().equals(""))
{
comments = NO_COMMENTS;
}
eventBuilder.data(String.class, comments);
OutboundEvent event = eventBuilder.build();
eventOutput.write(event);
}</code></pre>
PostgreSQL是一個開源的關系型數據庫。它近期添加的其中一個特性是,捕獲并發送所有記錄的變更作為連接應用組件的入站負載。這個InSoR能力是以一對數據庫觸發器和函數配置實現的。針對我們的博客主題表進行如下配置:
CREATE OR REPLACE FUNCTION proc_topics_notify_trigger() RETURNS trigger AS $$
DECLARE
BEGIN
PERFORM pg_notify('topics_observer', json_build_object('topic_id', NEW.topic_id, 'topic_comments', NEW.comments)::text);
RETURN new;
END;
$$ LANGUAGE plpgsql
DROP TRIGGER trigger_topics_notify ON topics;
CREATE TRIGGER trigger_topics_notify AFTER INSERT OR UPDATE OR DELETE ON topics
FOR EACH ROW EXECUTE PROCEDURE proc_topics_notify_trigger()</code></pre>
假設,在有些用戶正在閱讀博客評論的同時,其中有人打算增加一條新的評論。
@POST
@Path("/cast")
@Consumes(MediaType.APPLICATION_JSON)
public void addComment(String newComment) throws Exception
{
if(newComment != null && !newComment.trim().equals(""))
{
ObjectMapper mapper = new ObjectMapper();
TopicComments topicComments;
String comments = BlogByPostgreSQL.getInstance().findComments(ConfigStringConstants.TOPIC_ID);
if(comments == null || comments.trim().equals(""))
{
topicComments = new TopicComments();
topicComments.addComment(newComment);
String topicCommentsStr = mapper.writeValueAsString(topicComments);
BlogByPostgreSQL.getInstance().addTopic(topicCommentsStr);
}
else
{
if(!comments.contains(newComment))
{
topicComments = mapper.readValue(comments, TopicComments.class);
topicComments.addComment(newComment);
String topicCommentsStr = mapper.writeValueAsString(topicComments);
BlogByPostgreSQL.getInstance().updateTopic(topicCommentsStr);
}
}
}
}</code></pre>
然后,數據庫中該記錄被這條新評論一改,該數據庫的“trigger_topics_notify”觸發器就將調用其相關的“proc_topics_notify_trigger”函數針對“topic_observer”發起一個變更事件通知。該“topic_observer”通知將立即推送給“topic_observer”的監聽者,連同JSON格式的更新評論一起作為數據負載。該應用組件與這些監聽者保持聯系,依次處理和編寫另外的SSE EventOutput到該控制器去刷新這些更新的評論到所有用戶視圖。這樣所有事就都已經實現了,不需要為用戶發起新的請求(如圖7)。
dWMVC的節點和RethinkDB模型組件
過去的幾年里,NodeJS已經成為用于構建Web應用服務器端運行期環境新銳選擇。它的核心架構是事件驅動、異步處理。RethinkDB是一個開源NoSQL數據庫,它將實時Web應用的開發放到它的架構和設計中進行了深思熟慮。其中一個內置的特性是,提供了變更事件的通知去調用應用組件。
對比圖6,下面的圖8最大的不同是數據庫觸發器和過程函數不再需要由RethinkDB來配置。它的數據庫變更事件的通知是用可鏈接(chainable)的查詢語言 ReQL 實現的。

圖8 基于NodeJS和RethinkDB數據庫的dWMVC模型組件。
圖9展示了應用和數據庫組件間的一系列交互和響應。
(點擊放大圖像)

圖9 基于NodeJS和RethinkDB數據庫,通過一系列交互為正在查看博客評論的用戶提供實時更新。
當服務器端組件blogApp.js 接收到一個基于SSE的getAllComments請求時,它首先按照新增的特定HTTP報頭準備響應,如下所示,為最初的響應去和dWMVC控制器握手。這使該控制器可以持續監聽后續SSE流事件。
function setUpSSE(res)
{
res.writeHead(200,
{
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Transfer-Encoding': 'chunked'
});
res.write('\n');
}
接下來,它通過BlogByRethinkDB.js執行一個可鏈接的ReQL查詢去通知該數據庫,它想要觀察和接收該數據記錄的未來任何的變更。這條可觀察的查詢使該數據庫一發生變更就將該變更 惰性流化 發回給應用組件。
BlogByRethinkDB.prototype.observeComments = function(process)
{
connectToRDB(function(err, rdbConn)
{
if(err)
{
if(rdbConn) rdbConn.close();
return console.error(err);
}
//Listen for blog comment change events
r.table(config.wmvcBlog.dbTable).filter(r.row('topicId').eq(config.wmvcBlog.wmvcBlogTopicId))
.changes({includeInitial: false}).run(rdbConn, function(err, cursor)
{
if(err)
{
if(rdbConn) rdbConn.close();
return console.error(err);
}
//Retrieve all the comments in an array.
cursor.each(function(err, row)
{
if(err)
{
if(rdbConn) rdbConn.close();
return console.error(err);
}
if(row)
{
return process(null, row.new_val);
}
});
});
});
};</code></pre>
然后,它重新獲取該請求主題的所有已有評論。
BlogByRethinkDB.prototype.getAllComments = function(process)
{
connectToRDB(function(err, rdbConn)
{
if(err)
{
if(rdbConn) rdbConn.close();
return console.error(err);
}
//Query for comments
r.table(config.wmvcBlog.dbTable).filter(r.row('topicId').eq(config.wmvcBlog.wmvcBlogTopicId))
.run(rdbConn, function(err, cursor)
{
if(rdbConn) rdbConn.close();
if(err)
{
return console.error(err);
}
//Retrieve all the comments in an array.
cursor.toArray(function(err, result)
{
if(err)
{
return console.error(err);
}
if(result && result.length > 0)
{
return process(null, result[0]);
}
else
{
return process(null, null);
}
});
});
});
};</code></pre>
最后,該服務器端對象組織并返回一個HTTP響應,它帶有SEE兼容格式的數據。
function handleSSEResponse(err, blogComments, res, next)
{
if(err)
{
return next(err);
}
if(blogComments) {
var id = new Date().getTime();
res.write('id: ' + id + '\n');
res.write('data: ' + JSON.stringify(blogComments) + '\n\n');
}
else
{
var empty = new Array();
var noResult = { comments: empty };
var id = new Date().getTime();
res.write('id: ' + id + '\n');
res.write('data: ' + JSON.stringify(noResult) + '\n\n');
}
}</code></pre>
其后,當新的評論添加到系統中時,observeComments將異步響應它的數據庫變更事件,并廣播該更新的評論給所有正在查看的用戶,如圖9所示。
pWMVC點對點Web應用
pWMVC架構方案的支柱是WebRTC協議。特別是其主要組件中的RTCDataChannel已在此博客應用的實現中得到了應用。該組件提供了在瀏覽器間雙向點對點數據傳輸的能力,而不需安裝額外的插件。
DataChannelJS 是一個針對RTCDataChannel的JavaScript包裝器類庫,用它可以使底層不必太過復雜,從而簡化實現。出現同樣的目的, PusherJS 被選擇來提供信號服務。WebRTC-aware的應用需要一個信號通道,用于特定客戶端交換會議描述和網絡可達性的信息。整個應用整合部署為一個NodeJS Web服務器。
需要注意的是,NodeJS服務器和PusherJS信號裝置都不保留瀏覽器間的任何數據交換。如圖10所示,參與其中的信息交換保存在每個用戶的瀏覽器 本地存儲 中。連同這些本地存儲一起,所有主要應用組件也都位于瀏覽器端,并在運行期執行。該NodeJS組件只在瀏覽器間轉播博客評論,維護組連接狀態,保持通信通道的開通。

圖10 該博客應用pWMVC實現的架構圖。所有應用評論和數據存儲都在用戶瀏覽器上。 Node.js的主要職責是負責所有參與者瀏覽器間的信號傳輸。
圖11 闡述了兩個用戶之間建立和形成博客主題組的順序過程流。第一個用戶在他的瀏覽器上訪問和初始化pWMVC應用,p2pController通過若干步驟打開了一個DataChannelJS實例,綁定到一個PusherJS信號頻道,并開始發送通信信號。此時,由于沒有其他同樣的參與者,該應用為首個用戶顯示一個默認頁面。接下來,另一位用戶打開該博客Web頁面。p2pCcontroller檢測該博客組已經打開,于是它就直接連接這第二位用戶的DataChannelJS并綁定它到PusherJS信號裝置。然后,這兩個瀏覽器進行一系列 ICE (交互式連接建立)通信并協商完成一次p2p握手。這個過程在瀏覽器控制臺窗口通過一塊接一塊的方式來表示,而出于簡潔考慮就不顯示細節了。在握手之后,這兩個用戶準備好私下交換信息了,此僅限于現在在它們之間開通的DataChannelJS。
(點擊放大圖像)

圖11 基于Pusher.js、DataChannel.js和Node.js(延續圖10), 兩個用戶瀏覽器之間建立基于WebRTC通信的一系列交互
一旦兩個用戶之間開通了DataChannelJS(如圖12),該應用就會首先從該瀏覽器本地存儲接收和顯示該主題已有的評論(如果有的話),以便他們了解上次交流至今錯過的內容。
webRTCDatachannel.onopen = function (userId)
{
p2pModel.getAllComments(groupName)
.success(function(updatedComments)
{
if(updatedComments === null)
{
updatedComments = { comments: new Array() };
}
$scope.comments = updatedComments;
})
.error(function(error)
{
alert('Failed to save the new comment' + error);
});
}
getAllComments: function (groupName)
{
var savedComments = $window.localStorage.getItem(groupName);
if(savedComments !== null)
{
savedComments = JSON.parse(savedComments);
}
var updatedComments = aggregateComments("", null, savedComments);
return handlePromise($q, updatedComments);
}</code></pre>
圖12 延續圖11,基于Pusher.js、DataChannel.js和Node.js,兩個用戶瀏覽器交換基于WebRTC信息的一系列交互。該信息保存在個人用戶瀏覽器的本地存儲中。
在這些用戶查看評論的同時,他們的瀏覽器會繼續給彼此發信號以保持通信通道的開通。因此,用戶可以發表其他新的評論,如圖12及下面的代碼片段所示。
$scope.addComment = function ()
{
var newInput = $scope.newComment.trim();
if (!newInput.length)
{
return;
}
var currentComments = $scope.comments;
p2pModel.aggregateAndStoreComments(groupName, newInput, currentComments)
.success(function(updatedComments)
{
webRTCDatachannel.send(updatedComments);
$scope.comments = updatedComments;
})
.error(function(error)
{
alert('Failed to save the new comment' + error);
});
$scope.newComment = '';
}</code></pre>
當新的評論發表出來時,p2pController首先使用p2pModel針對該主題聚集和更新本地存儲(如下所示)。然后,通過DataChannelJS將更新的評論發送給其他參與者。
aggregateAndStoreComments: function (groupName, comment, currentComments)
{
var savedComments = $window.localStorage.getItem(groupName);
if(savedComments !== null)
{
savedComments = JSON.parse(savedComments);
}
var updatedComments = aggregateComments(comment, currentComments, savedComments);
storeComments(groupName, updatedComments, $window);
return handlePromise($q, updatedComments);
}</code></pre>
當其他參與者接收到更新的評論時,評論被顯示在該Web頁面上,并保存到他們的本地存儲中。
webRTCDatachannel.onmessage = function (newInput, userId)
{
p2pModel.aggregateAndStoreComments(groupName, "", newInput)
.success(function(updatedComments)
{
$scope.comments = updatedComments;
})
.error(function(error)
{
alert('Failed to save the new comment' + error);
});
}
總結
盡管MVC架構方法的交互和響應的典范在萬維網前二十年期間的Web應用領域的應用減弱了,但近期的進步又使該基礎理論得以在Web開發社區復興。標準通信協議及其特有的InSoR能力使信息變更事件可以實時地動態和異步循環遍歷Web應用系統的邊界。這些使現代Web應用開發人員可以利用dWMVC和pWMVC架構范式去完成MVC-esque實時變更觀察“事件循環”,按多變的流行風尚創建無縫、高效的響應式應用行為。這些工具不僅可應用于現代新的服務器端運行期環境,也可以用于傳統的中間件架構。
來自:http://www.infoq.com/cn/articles/mvc-real-time-reactive-fulfillment