作者 冬雨
,译者本文要点
- 在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>
该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 = ''; };
接下来,在下面将讨论由服务器模型组件捕获和处理它所关联的数据变更。
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); }
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()
假设,在有些用户正在阅读博客评论的同时,其中有人打算增加一条新的评论。
@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); } } } }
然后,数据库中该记录被这条新评论一改,该数据库的“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); } }); }); }); };
然后,它重新获取该请求主题的所有已有评论。
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); } }); }); }); };
最后,该服务器端对象组织并返回一个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'); } }
其后,当新的评论添加到系统中时,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); }
图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 = ''; }
当新的评论发表出来时,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); }
当其他参与者接收到更新的评论时,评论被显示在该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实时变更观察“事件循环”,按多变的流行风尚创建无缝、高效的响应式应用行为。这些工具不仅可应用于现代新的服务器端运行期环境,也可以用于传统的中间件架构。
关于作者
Victor Chen 渴求追赶计算机领域的创新和改革。他具有虚拟现实、移动和Web应用方面的设计和实现的丰富经验。他还成功地设计和建造了数个竞赛机器人。
Brent Chen先生自上世纪九十年代以来就一直致力于系统架构和应用开发。他所提出的解决方案涵盖了众多的专业领域,其中包括:工资管理、人力资源管理、职工福利管理、监管合规、卫生保健和政府事务等。Brent Chen先生曾供职于一些主要的解决方案和服务提供商,诸如:Computer Sciences Corp、Northrop Grumman、ADP、LLC等。他的一个研究兴趣就是去探究当前正在发展的Web架构和技术中的新机遇和新兴前沿领域。
查看英文原文:Article: Polymorphism of MVC-esque Web Architecture: Real Time Reactive Fulfillment
转自 http://www.infoq.com/cn/articles/mvc-real-time-reactive-fulfillment