领先一步
VMware 提供培训和认证,以加快您的进度。
了解更多要查看此代码的更新,请访问我们的 React.js 和 Spring Data REST 教程。 |
在 上一节 中,您介绍了条件更新,以避免在编辑相同数据时与其他用户发生冲突。您还学习了如何在后端使用乐观锁来管理数据版本。如果您有人编辑了相同的记录,您会收到提示,以便您可以刷新页面并获取更新。
这很好。但是您知道什么更好吗?让 UI 在其他人更新资源时动态响应。
在本节中,您将学习如何使用 Spring Data REST 的内置事件系统来检测后端的更改,并通过 Spring 的 WebSocket 支持将更新发布给所有用户。然后,您将能够在数据更新时动态调整客户端。
随意从该存储库 获取代码 并继续学习。本节基于上一节的应用程序,并添加了一些额外内容。
在开始之前,您需要向项目的 pom.xml 文件添加一个依赖项
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
这引入了 Spring Boot 的 WebSocket 启动器。
Spring 提供了强大的 WebSocket 支持。需要注意的是,WebSocket 是一种非常底层的协议。它除了提供在客户端和服务器之间传输数据的方法之外,几乎没有其他作用。建议使用子协议(本节使用 STOMP)来实际编码数据和路由。
以下代码用于在服务器端配置 WebSocket 支持
@Component @EnableWebSocketMessageBroker public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
static final String MESSAGE_PREFIX = "/topic"; @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/payroll").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(MESSAGE_PREFIX); registry.setApplicationDestinationPrefixes("/app"); }
}
@EnableWebSocketMessageBroker
打开 WebSocket 支持。AbstractWebSocketMessageBrokerConfigurer
提供了一个方便的基类来配置基本功能。registerStompEndpoints()
用于配置后端客户端和服务器连接的端点 (/payroll
)。configureMessageBroker()
用于配置用于在服务器和客户端之间中继消息的代理。通过此配置,现在可以利用 Spring Data REST 事件并通过 WebSocket 发布它们。
Spring Data REST 基于在存储库上发生的 action 生成多个 应用程序事件。以下代码显示了如何订阅其中一些事件
@Component @RepositoryEventHandler(Employee.class) public class EventHandler {
private final SimpMessagingTemplate websocket; private final EntityLinks entityLinks; @Autowired public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) { this.websocket = websocket; this.entityLinks = entityLinks; } @HandleAfterCreate public void newEmployee(Employee employee) { this.websocket.convertAndSend( MESSAGE_PREFIX + "/newEmployee", getPath(employee)); } @HandleAfterDelete public void deleteEmployee(Employee employee) { this.websocket.convertAndSend( MESSAGE_PREFIX + "/deleteEmployee", getPath(employee)); } @HandleAfterSave public void updateEmployee(Employee employee) { this.websocket.convertAndSend( MESSAGE_PREFIX + "/updateEmployee", getPath(employee)); } /** * Take an {@link Employee} and get the URI using * Spring Data REST's {@link EntityLinks}. * * @param employee */ private String getPath(Employee employee) { return this.entityLinks.linkForSingleResource(employee.getClass(), employee.getId()).toUri().getPath(); }
}
@RepositoryEventHandler(Employee.class)
将此类标记为基于**员工**捕获事件。SimpMessagingTemplate
和 EntityLinks
从应用程序上下文自动装配。@HandleXYZ
注解标记需要监听的方法。这些方法必须是公共的。每个这些处理程序方法都调用SimpMessagingTemplate.convertAndSend()
通过 WebSocket 传输消息。这是一种发布-订阅方法,因此一条消息会被转发到每个连接的消费者。
每条消息的路由都不同,允许将多条消息发送到客户端上的不同接收器,而只需要一个打开的 WebSocket,这是一种资源高效的方法。
getPath()
使用 Spring Data REST 的 EntityLinks
来查找给定类类型和 ID 的路径。为了满足客户端的需求,此 Link
对象将转换为具有已提取路径的 Java URI。
注意
|
EntityLinks 带有几个实用程序方法,用于以编程方式查找各种资源的路径,无论是单个资源还是集合资源。 |
从本质上讲,您正在监听创建、更新和删除事件,并在它们完成后,将它们的通知发送给所有客户端。也可以在这些操作发生**之前**拦截它们,也许记录它们,由于某些原因阻止它们,或者用额外信息修饰域对象。(在下节中,我们将看到一个对此非常有用的用法!)
下一步是编写一些客户端代码来使用 WebSocket 事件。以下主要应用程序中的代码段引入了一个模块。
var stompClient = require('./websocket-listener')
该模块如下所示
define(function(require) { 'use strict';
var SockJS = require('sockjs-client'); <b class="conum">(1)</b> require('stomp-websocket'); <b class="conum">(2)</b> return { register: register }; function register(registrations) { var socket = SockJS('/payroll'); <b class="conum">(3)</b> var stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { registrations.forEach(function (registration) { <b class="conum">(4)</b> stompClient.subscribe(registration.route, registration.callback); }); }); }
});
/payroll
端点。registrations
数组,以便每个数组都可以在消息到达时订阅回调。每个注册条目都有一个route
和一个callback
。在下一节中,您可以看到如何注册事件处理程序。
在 React 中,组件的componentDidMount()
是在它在 DOM 中呈现后被调用的函数。这也是注册 WebSocket 事件的正确时机,因为组件现在已联机并准备就绪。查看下面的代码
componentDidMount: function () {
this.loadFromServer(this.state.pageSize);
stompClient.register([
{route: '/topic/newEmployee', callback: this.refreshAndGoToLastPage},
{route: '/topic/updateEmployee', callback: this.refreshCurrentPage},
{route: '/topic/deleteEmployee', callback: this.refreshCurrentPage}
]);
},
第一行与之前相同,其中所有员工都是使用页面大小从服务器获取的。第二行显示正在注册用于 WebSocket 事件的 JavaScript 对象数组,每个对象都有一个route
和一个callback
。
创建新员工时,行为是刷新数据集,然后使用分页链接导航到**最后一页**。为什么在导航到末尾之前刷新数据?添加新记录可能会导致创建新页面。虽然可以计算是否会发生这种情况,但这破坏了超媒体的意义。与其拼凑自定义页面计数,不如使用现有链接,并且只有在存在性能驱动的理由时才走这条路。
更新或删除员工时,行为是刷新当前页面。更新记录时,它会影响您正在查看的页面。当您删除当前页面上的记录时,下一页中的记录将被拉入当前页面,因此还需要刷新当前页面。
注意
|
这些 WebSocket 消息不需要以/topic 开头。它只是一个常见的约定,指示发布-订阅语义。 |
在下一节中,您可以看到执行这些操作的实际操作。
以下代码段包含两个回调,用于在收到 WebSocket 事件时更新 UI 状态。
refreshAndGoToLastPage: function (message) { follow(client, root, [{ rel: 'employees', params: {size: this.state.pageSize} }]).done(response => { this.onNavigate(response.entity._links.last.href); }) },
refreshCurrentPage: function (message) { follow(client, root, [{ rel: 'employees', params: { size: this.state.pageSize, page: this.state.page.number } }]).then(employeeCollection => { this.links = employeeCollection.entity._links; this.page = employeeCollection.entity.page;
return employeeCollection.entity._embedded.employees.map(employee => { return client({ method: 'GET', path: employee._links.self.href }) }); }).then(employeePromises => { return when.all(employeePromises); }).then(employees => { this.setState({ page: this.page, employees: employees, attributes: Object.keys(this.schema.properties), pageSize: this.state.pageSize, links: this.links }); });
},
refreshAndGoToLastPage()
使用熟悉的follow()
函数导航到应用了size 参数的employees 链接,插入this.state.pageSize
。收到响应后,您将调用上一节中的相同onNavigate()
函数,并跳转到**最后一页**,也就是可以找到新记录的页面。
refreshCurrentPage()
也使用follow()
函数,但将this.state.pageSize
应用于size,将this.state.page.number
应用于page。这将获取您当前正在查看的相同页面并相应地更新状态。
注意
|
此行为告诉每个客户端在发送更新或删除消息时刷新其当前页面。他们的当前页面可能与当前事件无关。但是,弄清楚这一点可能很棘手。如果删除的记录在第二页上,而您正在查看第三页怎么办?每个条目都会更改。但这是否是理想的行为?也许是,也许不是。 |
在本节结束之前,需要注意的是,您刚刚添加了一种新的方法来更新 UI 中的状态:当收到 WebSocket 消息时。但是,旧的更新状态的方法仍然存在。
为了简化代码的状态管理,请删除旧方法。换句话说,提交您的POST、PUT 和DELETE 调用,但不要使用它们的结果来更新 UI 的状态。而是等待 WebSocket 事件循环返回,然后进行更新。
以下代码段显示了与上一节相同的onCreate()
函数,只是简化了
onCreate: function (newEmployee) {
follow(client, root, ['employees']).done(response => {
client({
method: 'POST',
path: response.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
})
},
在这里,follow()
函数用于到达employees 链接,然后应用POST 操作。注意client({method: 'GET' …})
如何没有像以前那样的then()
或done()
?现在可以在您刚才查看过的refreshAndGoToLastPage()
中找到用于侦听更新的事件处理程序。
完成所有这些修改后,启动应用程序 (./mvnw spring-boot:run
) 并四处查看。打开两个浏览器标签页并调整大小,以便您可以同时看到它们。在一个标签页中开始进行更新,看看它们如何立即更新另一个标签页。打开您的手机并访问同一页面。找一个朋友并让他或她做同样的事情。您可能会发现这种动态更新更有吸引力。
想要一个挑战?尝试上一节中的练习,您在两个不同的浏览器标签页中打开相同的记录。尝试在一个标签页中更新它,并且**不要**在另一个标签页中看到它更新。如果可能的话,条件 PUT 代码仍然应该保护您。但这可能会更难以实现!
在本节中
凭借所有这些功能,很容易并排运行两个浏览器,并查看一个浏览器中的更新如何影响另一个浏览器。
问题?
虽然多个显示器可以很好地更新,但值得改进精确的行为。例如,创建一个新用户将导致所有用户跳转到末尾。关于如何处理这个问题有什么想法吗?
分页很有用,但提供了一种难以管理的状态。在这个示例应用程序上的成本很低,React 在更新 DOM 方面非常高效,不会导致 UI 中出现大量闪烁。但是对于更复杂的应用程序,并非所有这些方法都适用。
在考虑分页进行设计时,您必须决定客户端之间的预期行为以及是否需要更新。根据您的需求和系统的性能,现有的导航超媒体可能就足够了。