概述
在前后端分离架构中,后端提供RESTful API,前端使用Vue、React等框架独立开发,最后需要整合为一个可部署的应用。
本文基于一个使用Spring Security与OAuth2的后端项目,介绍如何将前端工程与后端工程合并为Maven多模块结构,实现统一编译、打包和运行。
纲要
- 前后端分离的多模块工程概览
- Maven父子工程配置
- 父POM:管理公共依赖与模块顺序
- 后端模块(UA):拷贝前端静态资源到classpath
- 前端模块(UA-UI):借助Maven插件执行Node构建
- 前端项目关键文件
package.json:脚本与依赖管理- 前端开发依赖与运行时依赖
- 工程启动与验证
- 架构图与代码示例
多模块工程结构
我们有两个独立工程:后端UA和前端UA-UI。通过在它们的上层目录新建一个聚合POM,形成父子工程,便于统一构建。
项目目录结构如下:
dir
parent/
├── pom.xml # 父POM
├── UA/ # 后端Spring Boot工程
│ ├── pom.xml
│ └── src/main/resources/public/ # 前端编译产物拷贝至此
└── UA-UI/ # 前端Vue工程
├── pom.xml
└── package.json
模块构建顺序:前端先编译生成HTML/JS/CSS,再由后端模块在package阶段拷贝到resources/public,最终打包成一个可执行的Jar。
#mermaid-svg-XcMMknxkoePPuWFm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XcMMknxkoePPuWFm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XcMMknxkoePPuWFm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XcMMknxkoePPuWFm .error-icon{fill:#552222;}#mermaid-svg-XcMMknxkoePPuWFm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XcMMknxkoePPuWFm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XcMMknxkoePPuWFm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XcMMknxkoePPuWFm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XcMMknxkoePPuWFm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XcMMknxkoePPuWFm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XcMMknxkoePPuWFm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XcMMknxkoePPuWFm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XcMMknxkoePPuWFm .marker.cross{stroke:#333333;}#mermaid-svg-XcMMknxkoePPuWFm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XcMMknxkoePPuWFm p{margin:0;}#mermaid-svg-XcMMknxkoePPuWFm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XcMMknxkoePPuWFm .cluster-label text{fill:#333;}#mermaid-svg-XcMMknxkoePPuWFm .cluster-label span{color:#333;}#mermaid-svg-XcMMknxkoePPuWFm .cluster-label span p{background-color:transparent;}#mermaid-svg-XcMMknxkoePPuWFm .label text,#mermaid-svg-XcMMknxkoePPuWFm span{fill:#333;color:#333;}#mermaid-svg-XcMMknxkoePPuWFm .node rect,#mermaid-svg-XcMMknxkoePPuWFm .node circle,#mermaid-svg-XcMMknxkoePPuWFm .node ellipse,#mermaid-svg-XcMMknxkoePPuWFm .node polygon,#mermaid-svg-XcMMknxkoePPuWFm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XcMMknxkoePPuWFm .rough-node .label text,#mermaid-svg-XcMMknxkoePPuWFm .node .label text,#mermaid-svg-XcMMknxkoePPuWFm .image-shape .label,#mermaid-svg-XcMMknxkoePPuWFm .icon-shape .label{text-anchor:middle;}#mermaid-svg-XcMMknxkoePPuWFm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XcMMknxkoePPuWFm .rough-node .label,#mermaid-svg-XcMMknxkoePPuWFm .node .label,#mermaid-svg-XcMMknxkoePPuWFm .image-shape .label,#mermaid-svg-XcMMknxkoePPuWFm .icon-shape .label{text-align:center;}#mermaid-svg-XcMMknxkoePPuWFm .node.clickable{cursor:pointer;}#mermaid-svg-XcMMknxkoePPuWFm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XcMMknxkoePPuWFm .arrowheadPath{fill:#333333;}#mermaid-svg-XcMMknxkoePPuWFm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XcMMknxkoePPuWFm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XcMMknxkoePPuWFm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XcMMknxkoePPuWFm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XcMMknxkoePPuWFm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XcMMknxkoePPuWFm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XcMMknxkoePPuWFm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XcMMknxkoePPuWFm .cluster text{fill:#333;}#mermaid-svg-XcMMknxkoePPuWFm .cluster span{color:#333;}#mermaid-svg-XcMMknxkoePPuWFm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XcMMknxkoePPuWFm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XcMMknxkoePPuWFm rect.text{fill:none;stroke-width:0;}#mermaid-svg-XcMMknxkoePPuWFm .icon-shape,#mermaid-svg-XcMMknxkoePPuWFm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XcMMknxkoePPuWFm .icon-shape p,#mermaid-svg-XcMMknxkoePPuWFm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XcMMknxkoePPuWFm .icon-shape .label rect,#mermaid-svg-XcMMknxkoePPuWFm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XcMMknxkoePPuWFm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XcMMknxkoePPuWFm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XcMMknxkoePPuWFm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 父POM
UA-UI 前端模块
UA 后端模块
maven-exec-plugin: npm install & build
target/dist
maven-resources-plugin 拷贝
spring-boot:repackage
可运行Jar
父POM配置
父POM继承自spring-boot-starter-parent,统一管理Spring Boot版本与公共属性,并在<modules>中声明两个子模块,前端模块放在第一位以保证编译顺序。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>ua-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>11</java.version>
<node.version>v16.14.2</node.version>
<npm.version>8.5.0</npm.version>
</properties>
<modules>
<module>UA-UI</module>
<module>UA</module>
</modules>
</project>
通过父POM,子模块无需重复声明Spring Boot的版本继承关系,公共属性也可复用。
后端模块配置
后端UA模块的pom.xml原本是独立Spring Boot项目,现在其<parent>指向父POM。为接收前端产物,添加maven-resources-plugin,在validate阶段将UA-UI编译输出目录的内容拷贝到src/main/resources/public,确保打包时静态资源位于classpath下。
xml
<!-- UA/pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>ua-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>UA</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- Spring Boot, Security, OAuth2 等依赖 -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-frontend</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/src/main/resources/public</outputDirectory>
<resources>
<resource>
<directory>${basedir}/../UA-UI/target/dist</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
确保UA-UI模块已执行构建,target/dist目录存在,否则拷贝会失败。通过这种方式,最终Jar包中的/public目录即前端静态文件,可直接通过浏览器访问。
前端模块配置
前端模块本质上是一个Node项目,Maven通过exec-maven-plugin调用系统安装的Node和npm,或者使用frontend-maven-plugin自动下载Node并执行构建。这里展示自动下载的方案,避免CI环境缺少Node的问题。
xml
<!-- UA-UI/pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>ua-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>UA-UI</artifactId>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.1</version>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
<npmVersion>${npm.version}</npmVersion>
<workingDirectory>${basedir}</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
该插件会自动下载指定版本的Node和npm,然后执行npm install和npm run build。npm run build命令对应前端package.json中定义的构建脚本。
前端关键文件:package.json
前端依赖和脚本通过package.json管理,类似于Java中的pom.xml。典型配置如下:
json
{
"name": "ua-ui",
"version": "1.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"vue": "^2.6.14",
"vue-router": "^3.5.1",
"vuex": "^3.6.2",
"axios": "^0.27.2",
"ant-design-vue": "^1.7.8"
},
"devDependencies": {
"@vue/cli-service": "^5.0.8",
"vue-template-compiler": "^2.6.14"
}
}
scripts定义了可执行命令,serve用于本地开发,build用于生产构建。dependencies是运行时依赖,如Vue全家桶、axios HTTP客户端、Ant Design Vue UI库。devDependencies是开发编译依赖,如Vue CLI构建工具和模板编译器。
前端项目启动时,开发者需预先安装Node环境,然后执行npm run serve即可在本地热加载开发。Maven整合后,npm run build会自动生成静态文件到target/dist目录。
启动与验证
调整目录结构后,确保后端application.properties或application.yml中的资源路径配置正确,并且Redis等外部依赖已启动。
在工程根目录执行统一构建:
bash
mvn clean package
构建过程:
- 父POM触发
UA-UI模块构建,插件安装Node并执行npm install与npm run build。 UA模块在validate阶段拷贝前端产物到src/main/resources/public,随后编译打包。- 生成的Jar包包含后端逻辑与前端静态资源,可直接运行:
bash
java -jar UA/target/UA-1.0.0.jar
访问 http://localhost:8080 即可看到前端页面,通过登录接口与后端OAuth2认证交互。前端使用axios发送HTTP请求,与后端的Spring Security安全体系对接,具体的认证流程将在后续章节展开。
总结
本文介绍了将独立的前后端工程整合为Maven多模块项目的方法,利用父POM统一管理依赖版本,借助frontend-maven-plugin实现Node环境标准化,通过资源拷贝将前端编译产物嵌入后端Jar包。
这种结构便于持续集成,使全栈应用成为一个整体交付单元。
读者可根据实际框架调整前端构建工具和目录布局,实现类似的一体化构建。