Making Spring/Tomcat compatible with HTML5 pushState

大城市里の小女人 提交于 2020-06-01 08:02:21

问题


I have a single-page web app that's using Backbone.js client routing with pushState. In order to get this to work, I have to tell my server (Java, Spring 3, Tomcat) which URLs should be resolved on the server (actual JSP views, API requets), and which should simply be sent to the index page to be handled by the client. Currently I'm using an InternalResourceViewResolver to simply serve JSP views that match the name of the URL request. Since client-side URLs don't have a view on the server, the server returns a 404.

What is the best way to specify to Spring (or Tomcat) that a few specific URLs (my client-side routes) should all resolve to index.jsp, and anything else should fall through to the InternalResourceViewResolver?


回答1:


I found that Spring MVC 3 added a tag that does exactly what I need, the mvc:view-controller tag. This got it done for me:

<mvc:view-controller path="/" view-name="index" />
<mvc:view-controller path="/admin" view-name="index" />
<mvc:view-controller path="/volume" view-name="index" />

http://static.springsource.org/spring/docs/3.0.x/reference/mvc.html




回答2:


In theory, to handle navigation via history.pushState you want to return index.html for unhandled resources. If you look at official documentation for modern web frameworks it's often realised based on 404 status.

In spring you should handle resources in order:

  • path mapped REST controllers
  • app static resources
  • index.html for others

To do this you have at least 4 possible solutions.

Using EmbeddedServletContainerCustomizer and custom 404 handler

@Controller
static class SpaController {
    @RequestMapping("resourceNotFound")
    public String handle() {
        return "forward:/index.html";
    }
}

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
    return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/resourceNotFound"));
}

Using custom default request mapping handler

@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;

static class SpaWithHistoryPushStateHandler {

}

static class SpaWithHistoryPushStateHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(final Object handler) {
        return handler instanceof SpaWithHistoryPushStateHandler;
    }

    @Override
    public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        response.getOutputStream().println("default index.html");
        return null;
    }

    @Override
    public long getLastModified(final HttpServletRequest request, final Object handler) {
        return -1;
    }
}

@Bean
public SpaWithHistoryPushStateHandlerAdapter spaWithHistoryPushStateHandlerAdapter() {
    return new SpaWithHistoryPushStateHandlerAdapter();
}

@PostConstruct
public void setupDefaultHandler() {
    requestMappingHandlerMapping.setDefaultHandler(new SpaWithHistoryPushStateHandler());
}

Using custom ResourceResolver

@Autowired
private ResourceProperties resourceProperties;

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**")
            .addResourceLocations(resourceProperties.getStaticLocations())
            .setCachePeriod(resourceProperties.getCachePeriod())
            .resourceChain(resourceProperties.getChain().isCache())
            .addResolver(new PathResourceResolver() {
                @Override
                public Resource resolveResource(final HttpServletRequest request, final String requestPath, final List<? extends Resource> locations, final ResourceResolverChain chain) {
                    final Resource resource = super.resolveResource(request, requestPath, locations, chain);
                    if (resource != null) {
                        return resource;
                    } else {
                        return super.resolveResource(request, "/index.html", locations, chain);
                    }
                }
            });
}

Using custom ErrorViewResolver

@Bean
public ErrorViewResolver customErrorViewResolver() {
    final ModelAndView redirectToIndexHtml = new ModelAndView("forward:/index.html", Collections.emptyMap(), HttpStatus.OK);
    return (request, status, model) -> status == HttpStatus.NOT_FOUND ? redirectToIndexHtml : null;
}

Summary

Fourth option looks simplest but as always it depends what you need. You may also want to restric returning index.html only when request expects text/html (which BasicErrorController already do based on "produces" header).

I hope one of this options will help in your case.




回答3:


I would give a clear scheme to my urls and separate frontend from backend.

Some suggestions:

  • Route all requests starting by /server to the backend and all others to the frontend.
  • Setup two different domains, one for the backend, one for the frontend.


来源:https://stackoverflow.com/questions/13295300/making-spring-tomcat-compatible-with-html5-pushstate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!