PageSourceSearch

https://blog.marcnuri.com/component---src-components-templates-pos…-using-maven-es-mdx-2bbf256ed99d724156ba.js

js marcnuri.com collected 2026-09-24 19:54:20 UTC 20,611 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunk_marcnuri_blog_marcnuri_com=self.webpackChunk_marcnuri_blog_marcnuri_com||[]).push([[3118],{29455:function(e,a,n){n.r(a),n.d(a,{default:function(){return i}});var t=n(28453),r=n(96540);function c(e){const a=Object.assign({h2:"h2",p:"p",a:"a",ul:"ul",li:"li",img:"img",code:"code",h3:"h3",pre:"pre",strong:"strong"},(0,t.RP)(),e.components);return r.createElement(r.Fragment,null,r.createElement(a.h2,{id:"introduction"},"Introducción"),"\n",r.createElement(a.p,null,"En este post te mostraré cómo servir una aplicación front-end ReactJS (o cualquier otra SPA de JavaScript) usando una aplicación Java Quarkus como back-end y servidor de páginas estáticas. El post también incluye instrucciones sobre cómo configurar Maven para realizar las tareas de construcción del front-end junto con el empaquetado de tu aplicación."),"\n",r.createElement(a.p,null,"Este artículo es muy similar a mi post de blog de 2017 ",r.createElement(a.a,{href:"post#angular-y-spring-boot-mediante-gradle"},"Integración de Angular + Spring Boot usando Gradle"),", con algunas características adicionales para habilitar el funcionamiento del router de React."),"\n",r.createElement(a.h2,{id:"requirements"},"Requisitos"),"\n",r.createElement(a.p,null,"Las librerías y tecnologías necesarias para este ejemplo son:"),"\n",r.createElement(a.ul,null,"\n",r.createElement(a.li,null,r.createElement(a.a,{href:"https://nodejs.org/"},"Node.js")),"\n",r.createElement(a.li,null,r.createElement(a.a,{href:"https://reactjs.org/"},"React")),"\n",r.createElement(a.li,null,r.createElement(a.a,{href:"https://quarkus.io/"},"Quarkus")),"\n",r.createElement(a.li,null,r.createElement(a.a,{href:"https://maven.apache.org/"},"Maven")),"\n"),"\n",r.createElement(a.p,null,"El objetivo principal es lograr un npm install + build que se integre con el plugin de recursos de Maven y despliegue los archivos de distribución a un directorio donde Quarkus pueda servirlos. Quarkus actuará entonces como el servicio back-end así como el gateway API para servir el front-end también."),"\n",r.createElement(a.p,null,"Puedes encontrar el código para estas configuraciones en mi aplicación quick start ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/tree/master/quickstarts/quarkus-dashboard"},"YAKC – Kubernetes Dashboard")," para YAKC (Yet Another Kubernetes Client)."),"\n",r.createElement(a.h2,{id:"back-end-(quarkus)"},"Back-end (Quarkus)"),"\n",r.createElement(a.p,null,"En este nuevo mundo de Microservicios, tiene sentido que el front-end y el/los back-end(s) sean servicios diferentes que usualmente son servidos y orquestados por un gateway API.\nEs difícil encontrar esas antiguas aplicaciones web donde el front-end y back-end eran parte del mismo servicio."),"\n",r.createElement(a.img,{src:"../../images/quarkus/microservice-vs-monolith-small.png",alt:"Una imagen mostrando un microservicio vs un monolito"}),"\n",r.createElement(a.p,null,"Esta arquitectura distribuida tiene sentido para proyectos empresariales grandes, pero a veces solo queremos hacer un prototipo o construir rápidamente una ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/tree/master/quickstarts/quarkus-dashboard"},"aplicación web simple"),".\nEse es el momento cuando empiezas a extrañar esos viejos monolitos y su simplicidad que te permitía crear y especialmente desplegar tu aplicación rápidamente."),"\n",r.createElement(a.p,null,"El objetivo principal es lograr que Quarkus actúe como un gateway API. Así que además de proporcionar la API REST, sirve los archivos del front-end también. De esta manera conseguimos mantener ReactJS como nuestra tecnología principal de front-end, Quarkus como nuestro back-end, y evitar todas las complejidades extra necesarias para un despliegue de 2 (o más) servicios separados."),"\n",r.createElement(a.p,null,"Puedes pensar que Quarkus ya proporciona una manera de servir ",r.createElement(a.a,{href:"https://quarkus.io/guides/http-reference#serving-static-resources"},"recursos estáticos")," y que la solución es tan simple como almacenar los archivos construidos del front-end en este directorio antes de construir el back-end. Sin embargo, esto no permitirá que el router de React funcione y no podrías enrutar ",r.createElement(a.code,null,"/nested/front-end/routed/paths")," (Solo intenta refrescar tu navegador en uno de esos paths anidados)."),"\n",r.createElement(a.h3,{id:"gatewayresource-class"},"Clase GatewayResource"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-java"},'@Path("/")\npublic class GatewayResource {\n\n  private static final Str
1ing FALLBACK_RESOURCE = "/frontend/index.html";\n  private final ApiResource apiResource;\n\n  @Inject\n  public GatewayResource(ApiResource apiResource) {\n    this.apiResource = apiResource;\n  }\n\n  @Path("/api/v1")\n  public ApiResource getApiResource() {\n    return apiResource;\n  }\n\n  @GET\n  @Path("/")\n  public Response getFrontendRoot() throws IOException {\n    return getFrontendStaticFile("index.html");\n  }\n\n  @GET\n  @Path("/{fileName:.+}")\n  public Response getFrontendStaticFile(@PathParam("fileName") String fileName) throws IOException {\n    final InputStream requestedFileStream = GatewayResource.class.getResourceAsStream("/frontend/" + fileName);\n    final InputStream inputStream = requestedFileStream != null ?\n      requestedFileStream :\n      GatewayResource.class.getResourceAsStream(FALLBACK_RESOURCE);\n    final StreamingOutput streamingOutput = outputStream -> IOUtils.copy(inputStream, outputStream);\n    return Response\n      .ok(streamingOutput)\n      .cacheControl(CacheControl.valueOf("max-age=900"))\n      .type(URLConnection.guessContentTypeFromStream(inputStream))\n      .build();\n  }\n\n}\n')),"\n",r.createElement(a.p,null,r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/GatewayResource.java#L36"},"Esta clase")," es el punto de entrada principal para cualquier solicitud HTTP que la aplicación recibe."),"\n",r.createElement(a.p,null,"El primer método, ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/GatewayResource.java#L50"},r.createElement(a.code,null,"getApiResource"))," delega cualquier solicitud a ",r.createElement(a.code,null,"/api/v1")," a la instancia ",r.createElement(a.a,{href:"https://docs.jboss.org/resteasy/docs/1.0.1.GA/userguide/html/JAX-RS_Resource_Locators_and_Sub_Resources.html"},"subresource")," ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/ApiResource.java#L31"},r.createElement(a.code,null,"ApiResource"))," que se encargará de ella."),"\n",r.createElement(a.p,null,"El ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/GatewayResource.java#L56"},r.createElement(a.code,null,"getFrontendRoot"))," se encargará de cualquier solicitud al path raíz ",r.createElement(a.code,null,"/")," solicitando el archivo ",r.createElement(a.code,null,"index.html")," al ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/GatewayResource.java#L62"},r.createElement(a.code,null,"getFrontendStaticFile"))," que describiré a continuación."),"\n",r.createElement(a.p,null,"Finalmente, el ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/GatewayResource.java#L62"},r.createElement(a.code,null,"getFrontendStaticFile"))," se encargará del resto de las solicitudes. El método primero trata de localizar cualquier archivo con el nombre de archivo proporcionado en el directorio de recursos ",r.createElement(a.code,null,"/frontend"),". Este directorio debería ser empaquetado durante la fase de construcción de la aplicación y contiene la aplicación ReactJS construida."),"\n",r.createElement(a.p,null,"Si no se encuentra ningún archivo con este nombre, servirá el archivo ",r.createElement(a.code,null,"index.html"),". Esta es la parte que permite que el ",r.createElement(a.strong,null,"router de React"),' funcione. Cualquier solicitud no coincidente ya sea por el subresource de API o un archivo de front-end servirá la aplicación React que luego se encargará de realizar un enrutamiento de "front-end".'),"\n",r.createElement(a.h2,{id:"front-end-(react)"},"Front-end (React)"),"\n",r.createElement(a.p,null,"Tu aplicación front-end completa puede estar ubicada en cualquier lugar dentro de tu proyecto, en mi caso la he ubicado bajo ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/tree/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/frontend"},r.createElement(a.code,null,"src/main/frontend")),". Además de notar la ubicación del path para tu aplicación React, realmente no necesitas ninguna configuración extra para que este procedimiento funcione."),"\n",r.createElement(a.h2,{id:"maven-build"},"Construcción Maven"),"\n",r.createElement(a.p,null,"Para el paso final de integrar el front-end React con el back-end Quarkus necesitas afinar la construcción Maven."),"\n",r.createElement(a.h3,{id:"resources"},"Recursos"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-xml"},"  \x3c!-- ... --\x3e\n  <build>\n    <resources>\n      <resource>\n        <directory>src/main/resources</directory>\n      </resource>\n      <resource>\n        <directory>src/main/frontend/build</directory>\n        <targetPath>frontend</targetPath>\n      </resource>\n    </resources>\n    \x3c!-- ... --\x3e\n  </build>\n  \x3c!-- ... --\x3e\n")),"\n",r.createElement(a.p,null,"En este ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/pom.xml#L62"},"primer extracto de código"),", he añadido una nueva entrada a la configuración de recursos de construcción.\nAdemás del estándar ",r.createElement(a.code,null,"src/main/resources"),", el directorio donde la aplicación React es construida ",r.createElement(a.code,null,"src/main/frontend/build")," también será ensamblado y copiado al directorio ",r.createElement(a.code,null,"target/classes/frontend"),".\nSi recuerdas, en la clase ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/java/com/marcnuri/yakc/quickstarts/dashboard/GatewayResource.java#L36"},r.createElement(a.code,null,"GatewayResource")),", estaba cargando recursos estáticos desde este directorio."),"\n",r.createElement(a.h3,{id:"npm-build-+-install"},"Npm build + install"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-xml"},"  \x3c!-- ... --\x3e\n  <profiles>\n    <profile>\n      <id>build-frontend</id>\n      <build>\n        <plugins>\n          <plugin>\n            <groupId>org.codehaus.mojo</groupId>\n            <artifactId>exec-maven-plugin</artifactId>\n            <version>${version.exec-maven-plugin}</version>\n            <executions>\n              <execution>\n                <id>npm-install</id>\n                <phase>generate-resources</phase>\n                <goals>\n                  <goal>exec</goal>\n                </goals>\n                <configuration>\n                  <workingDirectory>src/main/frontend</workingDirectory>\n                  <executable>
1npm</executable>\n                  <arguments>\n                    <argument>install</argument>\n                  </arguments>\n                </configuration>\n              </execution>\n              <execution>\n                <id>npm-build</id>\n                <phase>generate-resources</phase>\n                <goals>\n                  <goal>exec</goal>\n                </goals>\n                <configuration>\n                  <workingDirectory>src/main/frontend</workingDirectory>\n                  <executable>npm</executable>\n                  <arguments>\n                    <argument>run</argument>\n                    <argument>build</argument>\n                  </arguments>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    \x3c!-- ... --\x3e\n  </profiles>\n")),"\n",r.createElement(a.p,null,"El ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/pom.xml#L87"},"segundo extracto de código")," se usa para ejecutar ",r.createElement(a.code,null,"npm install")," y ",r.createElement(a.code,null,"npm run build")," desde Maven. Añadí esto como un perfil separado porque usualmente realizo estos pasos manualmente mientras estoy en mi máquina local. Solo los invoco en el CI, así que mi paso de construcción de CI se ve como ",r.createElement(a.code,null,"mvn -Pbuild-frontend clean package"),". Sin embargo, no hay problema en añadir estas configuraciones directamente a la sección de construcción de tu ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/pom.xml#L2"},r.createElement(a.code,null,"pom.xml")),"."),"\n",r.createElement(a.p,null,"Como puedes ver configuré 2 ejecuciones para el Exec Maven Plugin. La ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/pom.xml#L96"},"primera")," invocará ",r.createElement(a.code,null,"npm install")," usando el directorio home del front-end (",r.createElement(a.code,null,"src/main/frontend"),") como su directorio de trabajo. La ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/pom.xml#L110"},"segunda ejecución")," tiene una configuración similar pero ejecuta ",r.createElement(a.code,null,"npm run build")," en su lugar."),"\n",r.createElement(a.h2,{id:"execution"},"Ejecución"),"\n",r.createElement(a.p,null,"Una vez que tu aplicación está empaquetada, puedes ejecutarla ya sea ejecutando el archivo jar generado (",r.createElement(a.code,null,"java -jar ./target..."),") o ejecutando ",r.createElement(a.code,null,"mvn quarkus:dev"),"."),"\n",r.createElement(a.img,{src:"../../images/yakc/yakc-kubernetes-dashboard-quarkus-gateway.gif",alt:"Una grabación de cómo funciona el enrutamiento del front-end con Quarkus"}),"\n",r.createElement(a.h2,{id:"development-mode"},"Modo de desarrollo"),"\n",r.createElement(a.p,null,"Si quieres disfrutar y beneficiarte de la recarga en caliente/vivo tanto de Quarkus como de React necesitarás alguna configuración extra."),"\n",r.createElement(a.p,null,"En este caso, ejecutaremos el front-end usando el estándar ",r.createElement(a.code,null,"npm start")," y apuntando nuestro navegador a ",r.createElement(a.code,null,"http://localhost:3000"),". Y ejecutaremos nuestro back-end usando ",r.createElement(a.code,null,"mvn quarkus:dev")," en ",r.createElement(a.code,null,"http://localhost:8080"),"."),"\n",r.createElement(a.h3,{id:"back-end"},"Back-end"),"\n",r.createElement(a.p,null,"Dado que el front-end es servido desde una URL diferente, necesitamos habilitar CORS en Quarkus."),"\n",r.createElement(a.p,null,"Podemos hacer esto modificando nuestro ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/resources/application.properties#L1"}
1,r.createElement(a.code,null,"application.properties"))," para incluir:"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-properties"},"quarkus.http.cors=true\nquarkus.http.cors.origins=http://localhost:3000\nquarkus.http.cors.headers=accept, origin, authorization, content-type, x-requested-with\nquarkus.http.cors.methods=GET,POST,DELETE,OPTIONS\n")),"\n",r.createElement(a.p,null,"Estas propiedades habilitan CORS para permitir solicitudes desde el origen de desarrollo del front-end. Puedes modificar las ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/resources/application.properties#L2-L4"},"entradas")," para hacerlo más o menos restrictivo dependiendo de tus necesidades."),"\n",r.createElement(a.h3,{id:"front-end"},"Front-end"),"\n",r.createElement(a.p,null,"Para la parte del front-end, necesitamos afinar nuestra aplicación React para acceder a diferentes endpoints del back-end dependiendo del entorno en el que la estemos ejecutando."),"\n",r.createElement(a.p,null,"En mi caso, dado que estoy usando ",r.createElement(a.a,{href:"https://github.com/facebook/create-react-app"},"create-react-app"),", añadí dos ",r.createElement(a.a,{href:"https://create-react-app.dev/docs/adding-custom-environment-variables/#what-other-env-files-can-be-used"},"archivos de propiedades de entorno"),":"),"\n",r.createElement(a.p,null,r.createElement(a.code,null,".env")," para ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/frontend/.env"},"producción"),":"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-bash"},"REACT_APP_API_URL=/api/v1\n")),"\n",r.createElement(a.p,null,r.createElement(a.code,null,".env.development")," para ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/frontend/.env.development"},"desarrollo"),":"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-bash"},"REACT_APP_API_URL=http://localhost:8080/api/v1\n")),"\n",r.createElement(a.p,null,"Una vez que estos dos archivos están listos, create-react-app se encarga de los reemplazos apropiados en tu aplicación dependiendo de tu entorno objetivo. Para usar esos valores, necesitarás ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/blob/6f89e826972a62689c5dfc537c68ecae26f3c70b/quickstarts/quarkus-dashboard/src/main/frontend/src/env.js#L18"},"referenciar")," la URL usando la variable ",r.createElement(a.code,null,"process.env.REACT_APP_API_URL"),":"),"\n",r.createElement(a.pre,null,r.createElement(a.code,{className:"language-javascript"},"const requestDelete = async pod => {\n  await fetch(\n    `${process.env.REACT_APP_API_URL}/pods/${metadata.selectors.namespace(pod)}/${metadata.selectors.name(pod)}`,\n      {method: 'DELETE'}\n        );\n      }\n")),"\n",r.createElement(a.h2,{id:"conclusion"},"Conclusión"),"\n",r.createElement(a.p,null,"En este post, te he mostrado cómo servir tanto una aplicación front-end JavaScript como una aplicación back-end Java usando Quarkus.\nQuarkus actúa como un gateway API y ya sea sirve los archivos estáticos del front-end o procesa la solicitud del back-end usando recursos y servicios.\nTambién te he enseñado cómo combinar y usar la recarga en caliente en React y Quarkus afinando la configuración para cada aplicación."),"\n",r.createElement(a.p,null,"He extraído el código para este post de mi ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/tree/master/quickstarts/quarkus-dashboard"},"YAKC – Kubernetes Dashboard quickstart"),".\nPuedes aprender más visitando ",r.createElement(a.a,{href:"https://github.com/manusa/yakc"},"el sitio del proyecto GitHub de YAKC"),".\nTambién puedes ver el código relacionado ",r.createElement(a.a,{href:"https://github.com/manusa/yakc/tree/master/quickstarts/quarkus-dashboard"},"aquí"),"."))}var o=function(e){void 0===e&&(e={});const{wrapper:a}=Object.assign({},(0,t.RP)(),e.components);return a?r.createElement(a,e,r.createElement(c,e)):c(e)},s=n(37698);const l=e=>
1r.createElement(s.TP,{pageContext:e.pageContext},r.createElement(s.n$,Object.assign({comments:e.pageContext.comments,frontmatter:e.pageContext.frontmatter},e)));function i(e){return r.createElement(l,e,r.createElement(o,e))}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.