Vue 3 和 Jooby 看似分属两个世界:一个是主流的前端框架,一个是基于 JVM 的模块化微 Web 框架。但在实际项目中,这套组合的契合度相当高。Vue 3 的组合式 API 让前端代码可以按业务域拆分成一个个独立的 composable 和模块,而 Jooby 则用 Kotlin 或 Java 提供轻量、启动快、路由可分组的后端能力。把两者放进同一个工程化体系里,就能得到一套从目录结构、开发代理到部署托管都条理清晰的模块化全栈方案。本文将从前端组织、后端模块拆分、联调与部署三个层面展开。

一、Vue 3 侧的模块化工程组织
模块化的第一步是目录设计。很多 Vue 3 项目后期难以维护,根源在于一开始就把所有组件平铺在 components 目录下,业务模块之间相互引用,边界逐渐消失。更合理的做法是按业务域垂直切分,每个业务模块内部自带自己的组件、composable、类型定义和 API 封装,只有真正跨模块复用的部分才提升到公共层。
一个典型的目录结构如下:
src/ ├── modules/ │ ├── user/ │ │ ├── components/UserList.vue │ │ ├── composables/useUserList.ts │ │ ├── api/index.ts │ │ └── types.ts │ └── order/ │ ├── components/OrderDetail.vue │ ├── composables/useOrder.ts │ ├── api/index.ts │ └── types.ts ├── shared/ │ ├── request.ts │ └── utils/ ├── router/index.ts └── main.ts
这种垂直切分带来的直接好处是:删除或重构一个业务模块时,只需要处理 modules 下对应的目录,不会牵连其他部分。路由层也建议做同样的拆分,每个模块导出自己的路由片段,在入口处统一合并:
// modules/user/routes.ts
import UserList from './components/UserList.vue'
export default [
{
path: '/users',
name: 'user-list',
component: UserList,
},
]
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import userRoutes from '@/modules/user/routes'
import orderRoutes from '@/modules/order/routes'
const router = createRouter({
history: createWebHistory(),
routes: [...userRoutes, ...orderRoutes],
})
export default router
接口请求层同样要模块化。在 shared 目录里维护一个统一的请求实例,各模块的 api 目录只负责定义自己的接口路径和参数类型,避免请求配置散落各处:
// shared/request.ts
import axios from 'axios'
export const http = axios.create({
baseURL: '/api',
timeout: 10000,
})
// modules/user/api/index.ts
import { http } from '@/shared/request'
import type { User } from '../types'
export function fetchUsers() {
return http.get<User[]>('/users')
}
二、Jooby 侧的模块化路由设计
Jooby 的核心设计理念就是模块化。它不像传统 Servlet 框架那样把所有配置集中在一处,而是允许每个业务模块继承 Jooby 类或实现 Kooby 风格的扩展点,各自声明路由、依赖和中间件,最后在主应用中挂载。这种组织方式与前端的垂直切分形成呼应。
以 Kotlin DSL 为例,一个用户模块可以这样写:
// modules/UserModule.kt
import io.jooby.Kooby
import io.jooby.Jooby
class UserModule : Kooby({
path("/api/users") {
get {
val users = userService.findAll()
listOf(mapOf("id" to 1, "name" to "张三"))
}
get("/{id}") {
val id = ctx.path("id").intValue
userService.findById(id)
}
post {
val body = ctx.body(User::class.java)
userService.save(body)
}
}
})
主应用负责组装这些模块,并统一处理跨域、JSON 序列化等横切关注点:
// Application.kt
import io.jooby.Jooby
import io.jooby.json.JacksonModule
import io.jooby.CORS
fun main() {
Jooby.run {
install(JacksonModule())
cors(CORS().setAllowOrigin("*").setAllowMethods("GET", "POST", "PUT", "DELETE"))
mount(UserModule())
mount(OrderModule())
}
}
这种写法的价值在于模块的可插拔性。开发阶段可以只挂载当前正在联调的模块,缩短启动时间;测试阶段可以为每个模块编写独立的集成测试;上线后如果某个模块流量激增,也可以比较容易地把它拆成独立服务。Jooby 基于 Netty 的运行时让单模块启动通常在一秒以内,对本地开发迭代非常友好。
另一个值得注意的点是接口约定。建议在项目里用一份共享的类型定义文档约束两端,例如通过 OpenAPI 注解让 Jooby 自动生成接口描述,再用命令行工具生成前端的 TypeScript 类型文件。这样后端改字段时,前端在编译期就能收到报错,而不是等到联调时才发现数据结构对不上。
三、开发联调与生产部署的打通
开发阶段最常见的问题是跨域。与其在 Jooby 侧放开 CORS,不如直接用 Vite 的代理能力把请求转发到本地后端,这样浏览器看到的所有请求都来自同一个源:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
前端跑在 5173 端口,Jooby 跑在 8080 端口,开发时保存代码两边都能热更新,联调体验非常顺畅。前端改组件由 Vite 热替换接管,后端改路由则由 Jooby 配合构建工具增量重启。
到了生产部署,最省事的方案是让 Jooby 直接托管前端构建产物。Vue 3 执行 npm run build 后会生成 dist 目录,在 Jooby 中把它注册为静态资源目录即可:
// 生产环境托管前端产物
assets(Location.fileSystem("dist"), "/*")
// SPA 路由回退,避免刷新页面时 404
get("/{path:.*}") {
if (ctx.path().value().startsWith("/api")) {
ctx.send(404)
} else {
}
}
需要注意 SPA 的路由回退问题:前端使用了 history 模式路由,用户直接访问 /users 刷新时,请求会先打到后端,Jooby 需要把非 API 路径统一回退到 index.html,否则会出现 404。上面的示例中通过路径前缀判断区分 API 请求和页面请求,是比较简单可靠的做法。
如果流量较大,也可以把前端产物放到 Nginx 层,让 Nginx 处理静态资源和回退,Jooby 只暴露 /api 前缀的后端接口。这种两层结构在水平扩容时更灵活,静态资源还可以进一步接入 CDN。
四、几点工程化实践建议
首先是环境配置分离。前端的 .env.development 与 .env.production 分别维护接口地址,后端则用 Jooby 的配置文件区分数据源和日志级别,避免把测试配置带到线上。其次是错误码统一,建议前后端约定一套错误响应结构,包含 code、message 和可选的 detail 字段,前端的请求拦截器统一处理特定错误码,业务代码只关心成功数据。最后是 monorepo 的取舍,如果团队规模较小,前端一个仓库加后端一个仓库即可,共享类型通过构建流程分发;只有当多个前端应用需要复用同一套后端模块时,才值得引入 monorepo 工具统一管理。
整体来看,Vue 3 加 Jooby 这套组合的核心思路是一致的:两边都用模块化组织代码,都保持轻量启动,都在边界处做清晰约定。把这三件事做好,即便项目规模增长,工程结构依然能够保持可读、可拆、可测试的状态。