如何使用 html 5 模式和 Spring webflux 处理页面刷新
How to handle a page refresh using html 5 mode & Spring webflux
我正在尝试实施此处描述的技术:use html5 mode with servlets with webflux。
简而言之,用户需要能够从他们的浏览器刷新页面,而不会从 Spring Boot.
重定向到 404
白标签页面
上面的教程依赖于使用 servlet 的 forward:
机制的技术:
@Controller
public class ForwardController {
@RequestMapping(value = "/**/{[path:[^\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
但是我使用 webflux 而不是 servlets。这是我尝试使用 WebFilter
:
@Component
public class SpaWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api") && path.matches("[^\\.]*")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/").build()
).build());
}
return chain.filter(exchange);
}
}
当用户刷新页面时,这会导致 404
。
编辑:让我更详细地描述这个问题:
在浏览器中加载 SPA 后,用户可以使用 angular 路线链接进行导航。说从 http://localhost:8080/
到 http://localhost:8080/user-list
(这里 /user-list
是一条 angular 路线。此导航与后端没有交互。
现在,当用户 - 仍在 /user-list
路线上 - 选择刷新浏览器页面时,Spring 将尝试将 /user-list
路径解析为后端 handler/router 函数,这将导致从 Spring Boot.
提供 404 白标签错误页面
我想要实现的是,当用户刷新浏览器页面时,http://localhost:8080/user-list
页面仍然显示给用户。
编辑 2:请注意,此刷新问题不会出现在索引页 (http://localhost:8080/
) 上,因为我已实施此过滤器:
@Component
public class IndexWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getURI().getPath().equals("/")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()
).build()
);
}
return chain.filter(exchange);
}
}
为我的每个 Angular 路由实施一个这样的过滤器显然是不可行的...
编辑 3:另请注意,出现此问题是因为前端在后端类路径上作为 jar 使用以下配置提供服务:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/");
registry.addResourceHandler("/").addResourceLocations("classpath:/index.html");
}
}
换句话说,我既不使用前端代理也不使用反向代理(例如 nginx)
我找到了解决问题的办法。我弄错的是 url "forwarded" 的值。
通过使用 /index.html
而不是 /
,该应用的行为符合预期。
@Component
public class SpaWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api") && path.matches("[^\\.]*")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()
).build());
}
return chain.filter(exchange);
}
}
同样可以使用 NGINX 实现如下:
location / {
try_files $uri $uri/ /index.html;
}
这假定 angular 路由不能包含任何点并且不能以 /api
前缀开头。
这是我找到的有效解决方案:
https://github.com/emmapatterson/webflux-kotlin-angular/blob/master/README.md
希望对您有所帮助!
主要代码为:
import org.springframework.context.annotation.Configuration
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
private const val INDEX_FILE_PATH = "/index.html"
@Configuration
internal class StaticContentConfiguration(): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val path = exchange.request.uri.path
if (pathMatchesWebAppResource(path)) {
return redirectToWebApp(exchange, chain)
}
return chain.filter(exchange)
}
private fun redirectToWebApp(exchange: ServerWebExchange, chain: WebFilterChain) =
chain.filter(exchange.mutate()
.request(exchange.request.mutate().path(INDEX_FILE_PATH).build())
.build())
private fun pathMatchesWebAppResource(path: String) =
!path.startsWith("/api") && path.matches("[^\\.]*".toRegex())
}
我们可以通过web flux Router功能来实现,示例代码如下
@Bean
public RouterFunction<ServerResponse> htmlRouter(@Value("classpath:/static/index.html") Resource html) {
return route(
GET("/"),
request -> ok()
.contentType(MediaType.TEXT_HTML)
.bodyValue(html)
);
}
问题是当你刷新一个页面时,你需要用相同的url响应index.html,所以简单的重定向是不够的。
这是一个让我休息一天的解决方案:
@Configuration
class SpaConfig @Autowired constructor(
@Value("classpath:/frontend/index.html")
private val indexRes: Resource
) {
@Bean
fun spaResourceHandlerRegistrationCustomizer() = ResourceHandlerRegistrationCustomizer { registration ->
registration.resourceChain(true)
.addResolver(object : ResourceResolver {
override fun resolveResource(
exchange: ServerWebExchange?,
requestPath: String,
locations: MutableList<out Resource>,
chain: ResourceResolverChain
): Mono<Resource> = Mono.just(indexRes)
override fun resolveUrlPath(
resourcePath: String,
locations: MutableList<out Resource>,
chain: ResourceResolverChain
): Mono<String> = Mono.empty()
})
}
这样我们就不会硬编码“/api”或任何其他前缀。 index.html 与默认 spring ResourceWebHandler 一起提供,因此它支持最后修改的 header、范围等。浏览器中的 url 不会更改,因此不会破坏您的前端逻辑。来自 API 的 404 响应也得到了正确处理。
如果您想要与 MVC/servlet 世界中的处理方式类似的行为,您可以使用 org.springframework.util
中的 AntPathMatcher
。这样你就可以设置一个配置文件,或者像 /some-route/**
或 /another-route/**
这样的硬编码路由,这样你就可以处理嵌套的客户端路由,比如 /some-route/123/sub-route
。以下是我们在 Groovy 代码中处理它的方式:
client:
routes:
- /some-route/**
- /another-route/**
@Autowired
ClientSettings settings
@Override
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath()
if (evaluatePath(path)) {
return chain.filter(exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()).build())
}
chain.filter(exchange)
}
// probably a better way to do this
boolean evaluatePath(String path) {
def matcher = new AntPathMatcher()
def results = settings.routes.collect {antPattern ->
matcher.match(antPattern, path)
}
results.contains(true)
}
settings
是一个 @ConfigurationProperties
class,我们用“白名单”客户端路由列表注入。
我正在尝试实施此处描述的技术:use html5 mode with servlets with webflux。
简而言之,用户需要能够从他们的浏览器刷新页面,而不会从 Spring Boot.
重定向到404
白标签页面
上面的教程依赖于使用 servlet 的 forward:
机制的技术:
@Controller
public class ForwardController {
@RequestMapping(value = "/**/{[path:[^\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
但是我使用 webflux 而不是 servlets。这是我尝试使用 WebFilter
:
@Component
public class SpaWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api") && path.matches("[^\\.]*")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/").build()
).build());
}
return chain.filter(exchange);
}
}
当用户刷新页面时,这会导致 404
。
编辑:让我更详细地描述这个问题:
在浏览器中加载 SPA 后,用户可以使用 angular 路线链接进行导航。说从 http://localhost:8080/
到 http://localhost:8080/user-list
(这里 /user-list
是一条 angular 路线。此导航与后端没有交互。
现在,当用户 - 仍在 /user-list
路线上 - 选择刷新浏览器页面时,Spring 将尝试将 /user-list
路径解析为后端 handler/router 函数,这将导致从 Spring Boot.
我想要实现的是,当用户刷新浏览器页面时,http://localhost:8080/user-list
页面仍然显示给用户。
编辑 2:请注意,此刷新问题不会出现在索引页 (http://localhost:8080/
) 上,因为我已实施此过滤器:
@Component
public class IndexWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getURI().getPath().equals("/")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()
).build()
);
}
return chain.filter(exchange);
}
}
为我的每个 Angular 路由实施一个这样的过滤器显然是不可行的...
编辑 3:另请注意,出现此问题是因为前端在后端类路径上作为 jar 使用以下配置提供服务:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/");
registry.addResourceHandler("/").addResourceLocations("classpath:/index.html");
}
}
换句话说,我既不使用前端代理也不使用反向代理(例如 nginx)
我找到了解决问题的办法。我弄错的是 url "forwarded" 的值。
通过使用 /index.html
而不是 /
,该应用的行为符合预期。
@Component
public class SpaWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api") && path.matches("[^\\.]*")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()
).build());
}
return chain.filter(exchange);
}
}
同样可以使用 NGINX 实现如下:
location / {
try_files $uri $uri/ /index.html;
}
这假定 angular 路由不能包含任何点并且不能以 /api
前缀开头。
这是我找到的有效解决方案:
https://github.com/emmapatterson/webflux-kotlin-angular/blob/master/README.md
希望对您有所帮助! 主要代码为:
import org.springframework.context.annotation.Configuration
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
private const val INDEX_FILE_PATH = "/index.html"
@Configuration
internal class StaticContentConfiguration(): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val path = exchange.request.uri.path
if (pathMatchesWebAppResource(path)) {
return redirectToWebApp(exchange, chain)
}
return chain.filter(exchange)
}
private fun redirectToWebApp(exchange: ServerWebExchange, chain: WebFilterChain) =
chain.filter(exchange.mutate()
.request(exchange.request.mutate().path(INDEX_FILE_PATH).build())
.build())
private fun pathMatchesWebAppResource(path: String) =
!path.startsWith("/api") && path.matches("[^\\.]*".toRegex())
}
我们可以通过web flux Router功能来实现,示例代码如下
@Bean
public RouterFunction<ServerResponse> htmlRouter(@Value("classpath:/static/index.html") Resource html) {
return route(
GET("/"),
request -> ok()
.contentType(MediaType.TEXT_HTML)
.bodyValue(html)
);
}
问题是当你刷新一个页面时,你需要用相同的url响应index.html,所以简单的重定向是不够的。
这是一个让我休息一天的解决方案:
@Configuration
class SpaConfig @Autowired constructor(
@Value("classpath:/frontend/index.html")
private val indexRes: Resource
) {
@Bean
fun spaResourceHandlerRegistrationCustomizer() = ResourceHandlerRegistrationCustomizer { registration ->
registration.resourceChain(true)
.addResolver(object : ResourceResolver {
override fun resolveResource(
exchange: ServerWebExchange?,
requestPath: String,
locations: MutableList<out Resource>,
chain: ResourceResolverChain
): Mono<Resource> = Mono.just(indexRes)
override fun resolveUrlPath(
resourcePath: String,
locations: MutableList<out Resource>,
chain: ResourceResolverChain
): Mono<String> = Mono.empty()
})
}
这样我们就不会硬编码“/api”或任何其他前缀。 index.html 与默认 spring ResourceWebHandler 一起提供,因此它支持最后修改的 header、范围等。浏览器中的 url 不会更改,因此不会破坏您的前端逻辑。来自 API 的 404 响应也得到了正确处理。
如果您想要与 MVC/servlet 世界中的处理方式类似的行为,您可以使用 org.springframework.util
中的 AntPathMatcher
。这样你就可以设置一个配置文件,或者像 /some-route/**
或 /another-route/**
这样的硬编码路由,这样你就可以处理嵌套的客户端路由,比如 /some-route/123/sub-route
。以下是我们在 Groovy 代码中处理它的方式:
client:
routes:
- /some-route/**
- /another-route/**
@Autowired
ClientSettings settings
@Override
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath()
if (evaluatePath(path)) {
return chain.filter(exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()).build())
}
chain.filter(exchange)
}
// probably a better way to do this
boolean evaluatePath(String path) {
def matcher = new AntPathMatcher()
def results = settings.routes.collect {antPattern ->
matcher.match(antPattern, path)
}
results.contains(true)
}
settings
是一个 @ConfigurationProperties
class,我们用“白名单”客户端路由列表注入。