如何使用 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,我们用“白名单”客户端路由列表注入。