下集:一条打包到底的静态部署之路
说完坑,再来讲正经操作。
这次我决定写得明明白白,清清楚楚,把那条“Spring Boot 扛着 Next.js 上线”的路子掰碎了揉细了告诉你。
无废话、无迷信、无AI幻觉。
第一步:Next.js 静态导出
项目根目录下的 next.config.js
要写得很清楚,不要留一丝幻想:
/** @type {import('next').NextConfig} */
const nextConfig = {output: 'export', // 重点!导出为纯静态trailingSlash: true, // 每个路径带斜杠,方便生成 index.htmlimages: {unoptimized: true // 否则图片优化会报错},
};module.exports = nextConfig;
然后直接命令:
npm run build
npx next export
这时候,会生成一个 out/
文件夹,里面是你真正能用来部署的东西。
结构大概长这样:
out/
├── admin/
│ └── login/
│ └── index.html
├── register/
│ └── index.html
├── index.html
├── _next/static/chunks/...js
└── ...
注意,每个页面其实就是一个路径+index.html 文件结构,所以根本不用搞什么“SPA跳转”那一套逻辑!
第二步:复制到Spring Boot资源目录
假设你后端目录结构是标准的:
backend/
└── src/└── main/└── resources/└── static/
那么执行这条命令:
cp -r out/* ../backend/src/main/resources/static/
清空 static 目录再复制比较稳妥。
第三步:配置 Security —— 非 /api/** 一律放行
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.cors(cors -> cors.configurationSource(corsConfigurationSource())).csrf(AbstractHttpConfigurer::disable).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeHttpRequests(auth -> auth.requestMatchers("/api/admin/**").hasRole("ADMIN").requestMatchers("/api/**").authenticated().anyRequest().permitAll()).authenticationProvider(authenticationProvider()).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);return http.build();
}
千万不要用 .anyRequest().authenticated()
,那是开发环境用来封杀一切的,不适合你现在这个"一切靠静态"的部署场景。
第四步:配置 WebConfig —— 精准转发到 index.html
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/**").addResourceLocations("classpath:/static/").resourceChain(true).addResolver(new PathResourceResolver() {@Overrideprotected Resource getResource(String resourcePath, Resource location) throws IOException {Resource requested = location.createRelative(resourcePath);if (requested.exists() && requested.isReadable()) {return requested;}// 对于非静态资源请求,尝试回退到 index.htmlif (resourcePath.contains(".")) {return null; // 明确要请求静态资源的,找不到就报404}if (resourcePath.startsWith("api/")) {return null; // 让Controller处理}return new ClassPathResource("/static/index.html");}});}
}
这个配置非常关键。它做到了:
- 能访问的静态资源直接返回
- 明确的 API 请求让 controller 去处理
- 只有“没有扩展名”的页面路径,比如
/admin/login
,才回退到index.html
AI 给你那种“全都 forward 到 index.html” 的 Controller 方案,在这个结构下是没必要、也极其危险的。会白屏、跳错页,还极难调试。
第五步:打包运行!
用 Maven 构建:
mvn clean package -DskipTests
运行:
java -jar target/backend-0.0.1-SNAPSHOT.jar
然后你可以打开:http://localhost:8082/
/
首页会显示出来/admin/login
会跳转到你静态导出的admin/login/index.html
/register/?token=abc123
会正确加载 register 页并保留查询参数/api/**
接口仍然走后端 Controller,毫无冲突
最终效果:
你得到了一个:
- 单一jar包即可部署
- 不需要Node环境
- 支持多页访问和刷新
- 前后端分工明确但部署合体
- 操作简明、老板无感知的系统
结语:这条路走通了,我回头一看
- AI教的套路,不一定错,但常常不适合你这种“野路子结合小团队现状”的打法
- 静态部署这条线,只要搞清楚目录结构和路径映射,真的比你想的简单
- Spring Boot 其实是个好平台,只要你别逼它理解前端的世界
这次,我们用文件系统打败了抽象逻辑。
前端静态导出 + 后端反向代理 + 安全放行策略,这三板斧下去,小项目不管你多么前后端分离,最终都能“一锅端”。
别再迷信什么服务拆分了,小项目,能合就合。
回头再看那些鼓吹“必须前后端分离”的架构课件……我只想笑着对他们说一句:
“你把前端扔进static再说这话。”