Springboot / Angular2 – ¿Cómo manejar las URL de HTML5?

Creo que esta es una pregunta simple, pero no pude encontrar una respuesta o al menos usar los términos correctos en la búsqueda.

Estoy configurando Angular2 y Springboot juntos. De forma predeterminada, Angular usará rutas como localhost:8080\dashboard y localhost:8080\dashboard\detail .

Me gustaría evitar el uso de ruta como hashs, si es posible. Como dice la documentación angular:

La función provideRouter del enrutador establece LocationStrategy en PathLocationStrategy, por lo que es la estrategia predeterminada. Podemos cambiar a HashLocationStrategy con una anulación durante el proceso de arranque si lo preferimos.

Y entonces…

Casi todos los proyectos de Angular 2 deben usar el estilo HTML 5 predeterminado. Produce URLs que son más fáciles de entender para los usuarios. Y conserva la opción de hacer la representación del lado del servidor más tarde.

El problema es que cuando bash acceder a localhost:8080\dashboard , Spring buscará alguna asignación de controlador a esta ruta, que no tendrá.

 Whitelabel Error Page There was an unexpected error (type=Not Found, status=404). No message available 

Inicialmente pensé en hacer que todos mis servicios estuvieran en localhost:8080\api y toda mi información estática en localhost:8080\app . ¿Pero cómo le digo a Spring que ignore las solicitudes a esta ruta de app ?

¿Hay una mejor solución con Angular2 o Boot?

Tengo una solución para usted, puede agregar ViewController para reenviar solicitudes a Angular desde Spring boot.

 import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class ViewController { @RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" }) public String index() { return "forward:/index.html"; } } 

aquí he redirigido todos mis angular2 (“/ bikes”, “/ milages”, “/ gallery”, “/ tracks”, “/ tracks / {id: \ w +}”, “/ location”, “/ about”, “/ tests”, “/ tests / new”, “/ tests / **”, “/ questions”, “/ answers”) a mi SPA Puede hacer lo mismo con su preject y también puede redirigir su página de error 404 a la página de índice como un paso más. ¡Disfrutar!

En mis aplicaciones Spring Boot (versión 1 y 2), mis recursos estáticos están en un solo lugar:

 src/main/resources/static 

static es una carpeta reconocida por Spring Boot para cargar recursos estáticos.

Entonces la idea es personalizar la configuración Spring MVC.
La forma más simple es usar la configuración de Spring Java.

Implemento WebMvcConfigurer para reemplazar addResourceHandlers() . Agrego un ResourceHandler único al ResourceHandler actual.
El controlador se asigna en cada solicitud y especifico classpath:/static/ como valor de ubicación del recurso (por supuesto, puede agregar otros si es necesario).
Agrego una clase anónima personalizada PathResourceResolver para anular getResource(String resourcePath, Resource location) .
Y la regla para devolver el recurso es la siguiente: si el recurso existe y es legible (entonces es un archivo), lo devuelvo. De lo contrario, de forma predeterminada devuelvo la página index.html . Cuál es el comportamiento esperado para manejar las URL de HTML 5.

Spring Boot 1.X Aplicación:

Extendiendo org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter es el camino.
La clase es un adaptador de la interfaz WebMvcConfigurer con métodos vacíos que permiten que las subclases anulen solo los métodos que les interesan.

Aquí está el código completo:

 import java.io.IOException; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.resource.PathResourceResolver; @Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**/*") .addResourceLocations("classpath:/static/") .resourceChain(true) .addResolver(new PathResourceResolver() { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { Resource requestedResource = location.createRelative(resourcePath); return requestedResource.exists() && requestedResource.isReadable() ? requestedResource : new ClassPathResource("/static/index.html"); } }); } } 

Aplicación Spring Boot 2.X:

org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter quedó en desuso.
Implementar directamente WebMvcConfigurer es el camino ahora, ya que todavía es una interfaz, pero ahora tiene métodos predeterminados (posibles gracias a una línea base de Java 8) y se puede implementar directamente sin necesidad del adaptador.

Aquí está el código completo:

 import java.io.IOException; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**/*") .addResourceLocations("classpath:/static/") .resourceChain(true) .addResolver(new PathResourceResolver() { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { Resource requestedResource = location.createRelative(resourcePath); return requestedResource.exists() && requestedResource.isReadable() ? requestedResource : new ClassPathResource("/static/index.html"); } }); } } 

Puede reenviar todos los recursos no encontrados a su página principal proporcionando ErrorViewResolver personalizado. Todo lo que necesita hacer es agregar esto a su clase @Configuration:

 @Bean ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() { return new ErrorViewResolver() { @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) { return status == HttpStatus.NOT_FOUND ? new ModelAndView("index.html", Collections.emptyMap(), HttpStatus.OK) : null; } }; } 

Puede reenviar todo lo no asignado a Angular usando algo como esto:

 @Controller public class ForwardController { @RequestMapping(value = "/**/{[path:[^\\.]*}") public String redirect() { // Forward to home page so that route is preserved. return "forward:/"; } } 

Fuente: https://stackoverflow.com/a/44850886/3854385

El servidor My Spring Boot para angular también es un servidor de puerta de enlace con las llamadas API a /api para no tener una página de inicio de sesión en frente de las páginas angulosas, puede usar algo como.

 import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; /** * This sets up basic authentication for the microservice, it is here to prevent * massive screwups, many applications will require more secuity, some will require less */ @EnableOAuth2Sso @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter{ @Override public void configure(HttpSecurity http) throws Exception { http .logout().logoutSuccessUrl("/").and() .authorizeRequests() .antMatchers("/api/**").authenticated() .anyRequest().permitAll().and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } } 

Para hacerlo más simple, puedes simplemente implementar ErrorPageRegistrar directamente.

 @Component public class ErrorPageConfig implements ErrorPageRegistrar { @Override public void registerErrorPages(ErrorPageRegistry registry) { registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/")); } } 

Esto reenviará las solicitudes a index.html.

 @Controller @RequestMapping("/") public class MainPageController { @ResponseStatus(HttpStatus.OK) @RequestMapping({ "/" }) public String forward() { return "forward:/"; } } 

Estos son los tres pasos que debes seguir:

  1. Implemente su propio bean TomcatEmbeddedServletContainerFactory y configure RewriteValve

      import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; ... import org.apache.catalina.valves.rewrite.RewriteValve; ... @Bean TomcatEmbeddedServletContainerFactory servletContainerFactory() { TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory(); factory.setPort(8080); factory.addContextValves(new RewriteValve()); return factory; } 
  2. Agregue un archivo rewrite.conf al directorio WEB-INF de su aplicación y especifique las reglas de reescritura. Aquí hay un ejemplo de contenido rewrite.conf, que estoy usando en la aplicación angular para aprovechar la PathLocationStrategy de angular (básicamente, simplemente redirijo todo al index.html, ya que solo usamos el arranque de spring para servir el contenido web estático, de lo contrario necesita filtrar sus controladores en la regla RewriteCond):

      RewriteCond %{REQUEST_URI} !^.*\.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$ RewriteRule ^(.*)$ /index.html [L] 
  3. Deshágase de useHash (o configúrelo en falso) a partir de sus declaraciones de enrutamiento:

      RouterModule.forRoot(routes) 

o

  RouterModule.forRoot(routes, {useHash: false}) 

reenviar todas las rutas angulares con index.html. Incluyendo base href.

 import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class ViewController { @RequestMapping({ "jsa/customer","jsa/customer/{id}",}) public String index() { return "forward:/index.html"; } } 

En mi caso, jsa es href base.