React.js 和 Spring Data REST:第四部分 - 事件

工程 | Greg L. Turnquist | 2015年10月13日 | ...
要查看此代码的更新,请访问我们的 React.js 和 Spring Data REST 教程

上一节 中,您介绍了条件更新,以避免在编辑相同数据时与其他用户发生冲突。您还学习了如何在后端使用乐观锁来管理数据版本。如果您有人编辑了相同的记录,您会收到提示,以便您可以刷新页面并获取更新。

这很好。但是您知道什么更好吗?让 UI 在其他人更新资源时动态响应。

在本节中,您将学习如何使用 Spring Data REST 的内置事件系统来检测后端的更改,并通过 Spring 的 WebSocket 支持将更新发布给所有用户。然后,您将能够在数据更新时动态调整客户端。

随意从该存储库 获取代码 并继续学习。本节基于上一节的应用程序,并添加了一些额外内容。

向项目添加 Spring WebSocket 支持

在开始之前,您需要向项目的 pom.xml 文件添加一个依赖项

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

这引入了 Spring Boot 的 WebSocket 启动器。

使用 Spring 配置 WebSockets

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 提供了一个方便的基类来配置基本功能。
  • MESSAGE_PREFIX 是您将在每个消息的路由前添加的前缀。
  • registerStompEndpoints() 用于配置后端客户端和服务器连接的端点 (/payroll)。
  • configureMessageBroker() 用于配置用于在服务器和客户端之间中继消息的代理。

通过此配置,现在可以利用 Spring Data REST 事件并通过 WebSocket 发布它们。

订阅 Spring Data REST 事件

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) 将此类标记为基于**员工**捕获事件。
  • SimpMessagingTemplateEntityLinks 从应用程序上下文自动装配。
  • @HandleXYZ 注解标记需要监听的方法。这些方法必须是公共的。

每个这些处理程序方法都调用SimpMessagingTemplate.convertAndSend() 通过 WebSocket 传输消息。这是一种发布-订阅方法,因此一条消息会被转发到每个连接的消费者。

每条消息的路由都不同,允许将多条消息发送到客户端上的不同接收器,而只需要一个打开的 WebSocket,这是一种资源高效的方法。

getPath() 使用 Spring Data REST 的 EntityLinks 来查找给定类类型和 ID 的路径。为了满足客户端的需求,此 Link 对象将转换为具有已提取路径的 Java URI。

注意
EntityLinks 带有几个实用程序方法,用于以编程方式查找各种资源的路径,无论是单个资源还是集合资源。

从本质上讲,您正在监听创建、更新和删除事件,并在它们完成后,将它们的通知发送给所有客户端。也可以在这些操作发生**之前**拦截它们,也许记录它们,由于某些原因阻止它们,或者用额外信息修饰域对象。(在下节中,我们将看到一个对此非常有用的用法!)

配置 JavaScript WebSocket

下一步是编写一些客户端代码来使用 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);
		});
	});
}

});

  1. 您引入了 SockJS JavaScript 库来通过 WebSockets 进行通信。
  2. 您引入了 stomp-websocket JavaScript 库来使用 STOMP 子协议。
  3. 在此处,WebSocket 指向应用程序的/payroll 端点。
  4. 迭代提供的registrations 数组,以便每个数组都可以在消息到达时订阅回调。

每个注册条目都有一个route 和一个callback。在下一节中,您可以看到如何注册事件处理程序。

注册 WebSocket 事件

在 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 开头。它只是一个常见的约定,指示发布-订阅语义。

在下一节中,您可以看到执行这些操作的实际操作。

React 到 WebSocket 事件并更新 UI 状态

以下代码段包含两个回调,用于在收到 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 =&gt; {
        return client({
            method: 'GET',
            path: employee._links.self.href
        })
    });
}).then(employeePromises =&gt; {
    return when.all(employeePromises);
}).then(employees =&gt; {
    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 消息时。但是,旧的更新状态的方法仍然存在。

为了简化代码的状态管理,请删除旧方法。换句话说,提交您的POSTPUTDELETE 调用,但不要使用它们的结果来更新 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 代码仍然应该保护您。但这可能会更难以实现!

回顾

在本节中

  • 您使用 SockJS 回退配置了 Spring 的 WebSocket 支持。
  • 您订阅了来自 Spring Data REST 的创建、更新和删除事件,以动态更新 UI。
  • 您发布了受影响的 REST 资源的 URI 以及上下文消息("/topic/newEmployee"、"/topic/updateEmployee" 等)。
  • 您在 UI 中注册了 WebSocket 侦听器以侦听这些事件。
  • 您将侦听器连接到处理程序以更新 UI 状态。

凭借所有这些功能,很容易并排运行两个浏览器,并查看一个浏览器中的更新如何影响另一个浏览器。

问题?

虽然多个显示器可以很好地更新,但值得改进精确的行为。例如,创建一个新用户将导致所有用户跳转到末尾。关于如何处理这个问题有什么想法吗?

分页很有用,但提供了一种难以管理的状态。在这个示例应用程序上的成本很低,React 在更新 DOM 方面非常高效,不会导致 UI 中出现大量闪烁。但是对于更复杂的应用程序,并非所有这些方法都适用。

在考虑分页进行设计时,您必须决定客户端之间的预期行为以及是否需要更新。根据您的需求和系统的性能,现有的导航超媒体可能就足够了。

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加快您的进度。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部