mvc react
重要要点
|
之前在MVC风格的Web体系结构的多态性:分类中 ,我们说明和讨论了WMVC(基于Web的MVC )体系结构范式的三类。 它们是服务器端WMVC(sWMVC),双WMVC(dWMVC)和对等WMVC(pWMVC)。 sWMVC本质上通常是静态的,但可以使用其他两个体系结构范例来构建实时的响应式Web应用程序组件。 在后续文件中,我们将利用这两个体系结构范例来设计和演示完全动态和React式的现代Web组件。
MVC体系结构方法的核心是可观察到的事件驱动的实现,这些实现是用户视图与视图所反映的相关真实或虚拟世界之间的同步。 无论用户有无其他命令,视图都将对不断变化的世界做出React。 从最初的桌面GUI到现代的增强和虚拟现实( AR和VR ),许多MVC实现中都体现了这一理想。 正如WMVC分类文章中所讨论的,在Web时代的前二十年中,此基本思想在Web应用程序中基本上不存在。 在这段时间内,基于sWMVC的方法主导着Web应用程序。 近年来,它在WUI(Web用户界面)应用程序开发社区中得到了振兴。 这项新趋势是由许多最新的技术产品和标准化协议推动的。
在本文中,我们将把其中的一些新进展付诸实践,以实现从WUI到后端SoR(记录源)存储库的异步,自然,无缝,高效的变化观察React性“事件循环”。 这里应用的关键支持技术是:
与以下讨论的源代码,可以发现这里在GitHub上。
用户故事
假设我们的客户想要一个基于浏览器的博客评论系统。 该Web应用程序应允许用户查看和发布有关博客主题的评论。
以下是概念性网页设计的屏幕截图,由三个子视图组成。 顶部显示博客主题,其后是评论条目和提交字段。 最后一部分是显示所有用户输入的注释的区域。
图1.博客评论屏幕截图设计。
该博客系统应包括两个独特的应用程序:
- 第一个应用程序应拥有博客评论的所有权,并将其存储在集中式存储库中。
- 第二个应用程序不会在中央数据库中存储任何用户评论,以确保用户隐私和客户责任。
该系统的第三个组件是将来自其他来源的博客评论集成和聚合到集中存储库中,该存储库将在将来开发。
所有应用程序用户均应具有博客评论的最新视图。
当用户阅读评论时,其他用户或通过集成添加的新评论应自动且立即显示在所有查看用户的网页上,而无需他或她的手动命令。
系统架构
具有集中式存储库的博客Web应用程序将使用dWMVC范例进行设计和开发。 总体而言,应用程序组件之间的通信将通过AngularJS,SSE,InSoR和CDC实现。 这些技术将使系统对集中式存储库中的记录的任何修改(通过此Web应用程序或将来的集成模块)做出React,并将更改实时交付给最终用户,如图2所示。
图2.集中式实时博客Web应用程序的系统架构。 客户端和服务器端之间的通信基于HTTP和SSE协议,而InSoR和CDC完成应用程序服务器和数据存储库之间的往返。
第二个Web应用程序将使用pWMVC方案实现(图3)。 它将成为使用户聚集在一起的推动者,而无需拥有所交换内容的所有权。
图3.点对点实时博客Web应用程序的系统架构。
dWMVC的集中式Web App
下面的图4概述了基于dWMVC的博客Web应用程序的设计。 在浏览器端,视图和控制器组件基于AngularJS。 服务器端技术堆栈的两种不同组合在本文中用于实现dWMVC模型组件。 左侧是传统的Java和Java EE基础结构堆栈,以及关系数据库PostgresSQL 。 NodeJS和RethinkDB用于说明基于JavaScript的服务器端运行时环境和NoSQL数据存储库的体系结构范例。 这些不同的服务器端设计和实现为相同的功能提供了两种不同的方法。 除了NodeJS的异步特性外,这种差异在InSoR和CDC中尤为明显,其中NoSQL数据库提供商拥有使用非标准化技术来创建创新解决方案 (例如惰性评估和 延迟加载 )。 从传统的中间件从业者到NodeJS / NoSQL爱好者,这两种实现方式还提供了满足Web开发社区不断变化的兴趣的技术风格。
图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事件时,将更新的博客评论与用户的查看页面推送并绑定。
所有这些交互和React都是通过以下代码片段完成的:
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模型组件
图6显示了这种传统技术堆栈中涉及的主要组件,它们是基于Java的中间件应用程序库和关系数据库的组合。
图6.基于Java和PostgreSQLdWMVC模型组件。
这些模型组件之间的交互和React事件在下面的图7中说明。 它显示了两个用户访问博客应用程序。
图7.基于Java和PostgreSQL关系数据库的一系列交互为查看博客评论的用户提供了实时的最新更新。
当用户打开博客网页时,dWMVC控制器立即实例化SSE实例,该实例启动与服务器的通信以检索博客评论。 如下所示,对相关的服务器组件进行了注释,以履行SSE请求并交付基于SSE的输出。 当服务器端组件从dWMVC控制器接收到基于SSE的请求时,它首先在数据库中查询现有注释,并将异步EventOutput广播到控制器以将注释显示给用户浏览器。 同时,服务器端组件添加了一个侦听器,以监听PostgreSQL数据库的“ topics_observer”,以便连续接收有关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的Node和RethinkDB模型组件
在过去的几年中,NodeJS已经成为用于构建Web应用程序的新的,著名的替代服务器端运行时环境。 它的核心体系结构是事件驱动的异步处理。 RethinkDB是一个开放源代码NoSQL数据库,它在将实时Web应用程序开发到其体系结构和设计中时考虑了很多问题。 其内置的独特功能之一是向调用应用程序组件提供更改事件的通知。
与图6相比,下面图8的主要区别之一是RethinkDB不再需要设置数据库触发器和过程功能。 数据库更改事件的通知是通过其可链接查询语言ReQL完成的
图8.基于NodeJS和RethinkDB数据库的dWMVC模型组件。
下面的图9显示了应用程序和数据库组件之间交互和响应行为的顺序。
图9.基于NodeJS和RethinkDB数据库的一系列交互为查看博客评论的用户提供了实时更新。
当服务器端组件blogApp.js收到基于SSE的getAllComments请求时,它首先准备通过向其初始响应添加特殊的HTTP标头(如下所示)来做出相应的响应,以与dWMVC控制器建立SSE握手。 这允许控制器继续侦听后续的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);
}
});
});
});
};
最后,服务器端对象撰写并返回带有SSE兼容格式数据的HTTP响应。
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的对等网络应用程序
pWMVC体系结构方案的骨干是WebRTC协议。 具体来说,此博客应用程序实现中使用了其主要组件之一RTCDataChannel。 此组件提供了在浏览器之间进行双向对等(P2P)数据传输的功能,而无需安装其他插件。
RTCDataChannelJavaScript包装器DataChannelJS用于避免底层复杂性并简化实现。 出于同样的原因,选择PusherJS提供信令服务。 一个支持WebRTC的应用程序需要一个信令通道,供参与的客户端交换有关其会话描述和网络可达性的信息。 整个应用程序粘合在一起,以部署到NodeJS Web服务器。
应该注意的是,NodeJS服务器和PusherJS信号器都不会持久化或保留浏览器之间交换的任何数据。 如图10所示,参与者之间交换的信息存储在每个用户的浏览器localStorage中。 与主要存储一起,所有主要的应用程序组件都位于浏览器中,并在运行时从浏览器执行。 NodeJS组件仅用于在浏览器之间中继博客评论,维护组连接状态,并保持通信通道畅通。
图10.博客应用程序的pWMVC实现的体系结构图。 所有应用程序注释和数据存储库都在用户浏览器上。 Node.js服务器的主要作用是在所有参与者浏览器之间发出信号。
图11说明了顺序过程流,其中两个用户建立并形成博客主题组。 当第一个用户在其浏览器上访问并初始化pWMVC应用程序时,p2pController会经历许多步骤来打开DataChannelJS实例,绑定到PusherJS信号通道并开始发送通信信号。 此时,由于没有其他对等参与者,因此应用程序向该第一用户显示默认页面。 接下来,另一个用户打开博客网页。 p2pCcontroller检测到博客组已打开,因此它仅连接了该第二个用户的DataChannelJS并将其绑定到PusherJS信号器。 之后,这两个浏览器立即进行了一系列ICE (交互式连接建立)通信和协商,以完成p2p握手。 此过程由浏览器控制台窗口上的输出块之后的块表示; 因此,为简洁起见,没有显示细节。 握手之后,这两个用户已准备好私下交换信息,并且仅在彼此之间交换信息,因为DataChannelJS现在已打开。
图11.两个用户浏览器之间的一系列交互,以基于Pusher.js,DataChannel.js和Node.js建立基于WebRTC的通信(续图10)。
一旦在两个用户之间打开DataChannelJS(图12),应用程序就会首先从浏览器localStorage中检索并显示有关此主题的现有注释(如果有的话),以便他们知道上次对话的结束位置。
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的消息。 消息保留在单个用户浏览器的localStorage中。
当这两个用户都在审阅评论时,他们的浏览器会继续相互发信号以保持通信通道畅通。 因此,用户可以发布其他新注释,如图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聚合和更新主题的localStorage(如下所示)。 然后,更新的评论通过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);
}
当其他参与者收到更新的评论时,这些评论将显示在网页上,并也存储在他们的localStorage中。
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);
});
}
摘要
尽管在万维网的前二十年中,Web应用程序中MVC架构方法的交互性和React性理想已减弱,但最近的发展使Web开发社区中的这一基本理论焕发了活力。 标准化的通信协议和专有的InSoR功能使信息更改事件可以实时,异步地跨Web应用程序系统的基础结构边界循环。 这些使现代Web应用程序开发人员能够利用dWMVC和pWMVC体系结构范例来实现和关闭MVC风格的实时变化观察“事件循环”,从而以可变的方式创建无缝有效的响应式应用程序行为。 这些功能不仅在现代的新型服务器端运行时环境中可用,而且在传统的中间件基础结构中也可用。
mvc react