Objectives
Create a single page app with Spring Boot and Vue.js with:
- One maven build for both frontend and backend.
- Frontend bundled into the Boot app.
- Use Vue router in router history mode, so that we don't have the
#
in the url bar.
Prerequisites
You'll need to install:
- npm (on macOS, you can simply
brew install npm
) -
vue-cli (
npm install -g @vue/cli
- JDK (This example uses java 11, but any will do, just change the java version when creating the spring project)
- httpie (optional. You can use https://start.spring.io to bootstrap your Spring project).
Step by step
Create a Spring Boot Project
From a terminal
$ http https://start.spring.io/starter.tgz \
artifactId==cafe \
javaVersion==11 \
language==kotlin \
name==Cafe \
dependencies==webflux,devtools,actuator \
baseDir==cafe | tar -xzvf -
This will give you a basic spring boot project under cafe/
.
Test the build to make sure it works:
$ ./mvnw test
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:cafe >--------------------------
[INFO] Building Cafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13.718 s
[INFO] Finished at: 2019-03-30T12:19:24+10:00
[INFO] ------------------------------------------------------------------------
Create a Vue project
Use vue-cli to generate a Hello World Vue CLI project.
$ cd src/main
$ vue create frontend \
--no-git -i '{
"useConfigFiles": false,
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-typescript": {
"classComponent": true,
"useTsWithBabel": true
},
"@vue/cli-plugin-eslint": {
"config": "standard",
"lintOn": [
"save"
]
}
},
"router": true,
"routerHistoryMode": true,
"cssPreprocessor": "node-sass"
}'
Configure javascript build output directory
Configure webpack so that compiled static content is under target
, in keeping with maven conventions. Spring Boot serves static resources from public
at the classpath root, so we'll take that into consideration as well.
Edit src/main/frontend/vue.config.js
:
module.exports = {
outputDir: '../../../target/frontend/public'
}
Configure the maven build to compile the Vue project
We need to make sure the built static resources end up in the correct place so the maven build and spring know about it
Configure the npm build
Add this plugin to your pom.xml
's plugins
section:
<project>
...
<build>
<plugins>
...
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.7.5</version>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v11.12.0</nodeVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm build</id>
<goals>
<goal>npm</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
<configuration>
<workingDirectory>${project.basedir}/src/main/frontend</workingDirectory>
<installDirectory>${project.build.directory}/node</installDirectory>
</configuration>
</plugin>
...
<plugins>
</build>
</project>
Test it out by running ./mvnw process-resources
. You should see the output of the npm build in target/frontend/
.
Add compiled static resources to the maven build
Add the generated static component as resource to your build, by adding a resources
section to your pom.xml
.
<project>
...
<build>
...
<resources>
<resource>
<directory>${project.build.directory}/frontend</directory>
</resource>
</resources>
...
</build>
</project>
Configure spring boot plugin to include static resources
Add this extra configuration
element to the spring-boot-maven-plugin
's config so it will be treated as part of the Spring Boot app.
<project>
...
<build>
<plugins>
...
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<folders>
<folder>${project.build.directory}/frontend</folder>
</folders>
</configuration>
</plugin>
...
<plugins>
</build>
</project>
Almost there! If you run ./mvnw spring-boot:run
and point your browser to http://localhost:8080/index.html, you should see half of the vue hello world page. We need to do some more work on the backend to things lined up properly.
Rewrite URLs to make router history mode work
Create a filter that routes everything that's not a bunch of pre-set paths to the static index page.
We will let boot handle the following paths:
-
/actuator
: Spring Boot's actuator has endpoints for health checks, metrics etc -
/api
: this app's backend API can go under this path -
/js
,/css
,/img
: static resources
package com.example.cafe.web
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
@Component
class VueRoutePathFilter : WebFilter {
companion object {
val BOOT_PATHS = listOf(
"/actuator/",
"/api/",
"/js/",
"/css/",
"/img/"
)
const val SPA_PATH = "/index.html"
}
override fun filter(exchange: ServerWebExchange,
chain: WebFilterChain): Mono<Void> {
if (isApiPath(exchange.request.uri.path)) {
return chain.filter(exchange)
}
return chain
.filter(exchange
.mutate()
.request(exchange.request
.mutate().path(SPA_PATH)
.build())
.build())
}
private fun isApiPath(path: String): Boolean {
return BOOT_PATHS.any { path.startsWith(it) }
}
}
You should now be able to hit http://localhost:8080 to get the vue Hello World page.
The sample code for this project is on GitHub. Enjoy!
Top comments (1)
Spring Boot Security is making blockage to do frontend page, how to handle that?