1. 项目背景与核心需求
最近在重构公司一个老项目时,遇到了前后端分离部署带来的协作效率问题。前端用Vue打包生成的dist需要单独部署到Nginx,而后端是Spring Boot服务。每次联调测试时,前端同学改个CSS样式都得重新部署一次Nginx,后端接口有变动也得单独发版,双方就像在玩打地鼠游戏。
于是我们开始思考:能否把前端静态资源直接打包进Spring Boot的jar包里?这样就能实现:
- 单jar包部署(运维同事狂喜)
- 接口和页面版本天然一致(再也不用听到"你刷新下缓存试试")
- 本地开发时
localhost:8080直接访问(告别CORS报错)
2. 技术方案选型
2.1 资源存放位置选择
经过对比三种常见方案,我们最终选择了resources/static方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| resources/public | Spring Boot默认静态目录 | 可能和已有API路径冲突 |
| resources/static | 优先级高于public | 需要手动配置资源映射 |
| resources/META-INF/resources | 兼容性最好 | 目录结构较深,维护不便 |
关键配置示例:
// 防止静态资源被拦截 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); }2.2 构建工具集成
2.2.1 Maven方案
在pom.xml中添加frontend-maven-plugin:
<plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>1.12.1</version> <executions> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> <configuration> <nodeVersion>v16.14.2</nodeVersion> </configuration> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> </execution> <execution> <id>npm build</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>run build</arguments> </configuration> </execution> </executions> </plugin>2.2.2 Gradle方案
对于Gradle用户,可以使用com.moowork.node插件:
plugins { id "com.moowork.node" version "1.3.1" } task appNpmInstall(type: NpmTask) { args = ['install'] } task appNpmBuild(type: NpmTask) { args = ['run', 'build'] } processResources.dependsOn appNpmBuild appNpmBuild.dependsOn appNpmInstall3. 完整实现流程
3.1 前端项目改造
- 修改vue.config.js:
module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/' : '/', outputDir: '../backend/src/main/resources/static', indexPath: 'index.html', assetsDir: 'assets' }- 解决路由冲突问题:
const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes })重要提示:一定要设置正确的publicPath,否则会出现JS/CSS资源404
3.2 后端项目调整
- 添加统一入口控制器:
@Controller public class FrontendController { @GetMapping(value = { "/", "/login", "/dashboard/**" // 所有前端路由路径 }) public String forward() { return "forward:/index.html"; } }- 配置静态资源缓存(可选):
# application.properties spring.resources.cache.cachecontrol.max-age=86400 spring.resources.cache.cachecontrol.no-cache=false3.3 构建流程优化
我们设计了多环境构建方案:
<profiles> <profile> <id>dev</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <frontend.env>development</frontend.env> </properties> </profile> <profile> <id>prod</id> <properties> <frontend.env>production</frontend.env> </properties> </profile> </profiles>对应前端package.json:
"scripts": { "build": "vue-cli-service build --mode ${frontend.env}" }4. 常见问题与解决方案
4.1 资源加载404问题
典型症状:
- 页面能打开但JS/CSS加载失败
- 控制台报错
Failed to load resource
排查步骤:
- 检查jar包内容:
jar tvf target/xxx.jar | grep static - 确认资源路径是否正确
- 检查Spring Security配置是否放行静态资源
4.2 路由刷新白屏
解决方案:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{spring:\\w+}") .setViewName("forward:/"); registry.addViewController("/**/{spring:\\w+}") .setViewName("forward:/"); } }4.3 性能优化技巧
- 开启Gzip压缩:
server.compression.enabled=true server.compression.mime-types=text/html,text/css,application/javascript- 使用版本号控制缓存:
// vue.config.js configureWebpack: { output: { filename: 'js/[name].[hash:8].js', chunkFilename: 'js/[name].[hash:8].js' } }5. 进阶实践
5.1 动态配置方案
有时候我们需要根据环境动态配置API地址:
// 在public目录下创建config.js window._env_ = { API_URL: 'http://localhost:8080/api' }; // 后端动态替换 @Controller public class ConfigController { @GetMapping("/config.js") public String getConfig(HttpServletRequest request) { return "window._env_ = " + JSON.toJSONString(config) + ";"; } }5.2 灰度发布方案
通过Spring Boot的Profile实现AB测试:
@Profile("!prod") @Controller public class NewVersionController { @GetMapping("/new-feature") public String newFeature() { return "forward:/new-version/index.html"; } }6. 监控与维护
6.1 版本健康检查
添加端点检查前端资源版本:
@RestController @RequestMapping("/api/version") public class VersionController { @GetMapping public String getVersion() { try { Resource resource = new ClassPathResource("static/version.txt"); return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); } catch (IOException e) { return "unknown"; } } }6.2 构建耗时优化
通过缓存node_modules提升CI速度:
# .gitlab-ci.yml cache: key: ${CI_PROJECT_ID} paths: - node_modules/ - frontend/node_modules/实际落地这个方案后,我们的部署流程从原来的15分钟缩短到3分钟,联调效率提升60%。不过要注意的是,这种架构适合中小型项目,对于超大型前端应用可能还是需要独立部署。